diff --git a/.dockerignore b/.dockerignore index ac2e52d..e43ad67 100644 --- a/.dockerignore +++ b/.dockerignore @@ -69,6 +69,12 @@ web/npm-debug.log* web/yarn-debug.log* web/yarn-error.log* +# Svelte specific +svelte/.svelte-kit/ +svelte/build/ +svelte/node_modules/ +svelte/npm-debug.log* + # Go specific proxy/vendor/ *.test diff --git a/.env.example b/.env.example index 85cba1b..7d5ecc0 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# Claude Code Monitor Configuration +# Claude Code Proxy Configuration # Server Configuration SERVER_HOST=127.0.0.1 @@ -25,7 +25,24 @@ ANTHROPIC_MAX_RETRIES=3 # AUTH_API_KEY_HEADER=x-api-key # AUTH_ALLOW_LOCALHOST_BYPASS=true +# Dashboard Auth (protects web UI and data endpoints with HTTP basic auth) +# When set, accessing the dashboard or /api/* data routes requires user "admin" +# with this password. Proxy endpoints (/v1/messages, /health) are NOT affected. +# DASHBOARD_PASSWORD=change-me-to-a-strong-password + +# Reverse-proxy deployments +# Set this to true when the proxy itself binds publicly but is only reachable +# through a trusted reverse proxy such as Traefik. +# TRUST_PROXY=true + # Storage Configuration +# DB_TYPE=sqlite +# DATABASE_URL=postgresql://user:password@localhost:5432/claude_code_proxy?sslmode=disable +# TEST_POSTGRES_DSN=postgresql://user:password@localhost:5432/claude_code_proxy_test?sslmode=disable +# TEST_POSTGRES_USER=test +# TEST_POSTGRES_PASSWORD=test +# TEST_POSTGRES_DB=claude_code_proxy_test +# TEST_POSTGRES_PORT=5434 DB_PATH=requests.db STORAGE_CAPTURE_REQUEST_BODY=true STORAGE_CAPTURE_RESPONSE_BODY=true @@ -34,6 +51,7 @@ STORAGE_RETENTION_DAYS=0 # STORAGE_REDACTED_FIELDS=api_key,authorization,token,password,secret,access_token,refresh_token,client_secret # CORS Configuration (comma-separated values) -# CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173 +# Defaults are permissive. Set these explicitly if you want tighter browser access. +# CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:5174,http://127.0.0.1:5174 # CORS_ALLOWED_METHODS=GET,POST,DELETE,OPTIONS # CORS_ALLOWED_HEADERS=Accept,Authorization,Content-Type,Anthropic-Version,Anthropic-Beta,X-API-Key,X-Requested-With diff --git a/.gitignore b/.gitignore index 759a730..1466813 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ # Dependencies node_modules/ +/web/node_modules/ +/svelte/node_modules/ /web/build/ /web/.cache/ +/svelte/.svelte-kit/ +/svelte/build/ # Environment files .env @@ -18,6 +22,7 @@ proxy.log bin/ dist/ *.exe +proxy/proxy # IDE and system files .DS_Store @@ -42,4 +47,4 @@ temp/ # Config -config.yaml \ No newline at end of file +config.yaml diff --git a/Dockerfile b/Dockerfile index 6891ded..18a1755 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,64 +1,132 @@ +# syntax=docker/dockerfile:1 # Multi-stage Dockerfile for Claude Code Proxy -# Builds both Go proxy server and Remix frontend in a single container +# Builds both Go proxy server and SvelteKit frontend in a single container +# +# Targets: +# - (default): Production runtime image +# - dev: Development image with hot-reload tooling -# Stage 1: Build Go Backend -FROM golang:1.21-alpine AS go-builder +# ============================================================================ +# Stage: go-builder — compile Go proxy binary +# ============================================================================ +FROM golang:1.26-alpine AS go-builder -WORKDIR /app +WORKDIR /app/proxy -# Install build dependencies including gcc for CGO +# Install build dependencies including gcc for CGO (sqlite) RUN apk add --no-cache git gcc musl-dev sqlite-dev -# Copy Go modules -COPY proxy/go.mod proxy/go.sum ./proxy/ -WORKDIR /app/proxy -RUN go mod download +# Copy Go modules first (cache layer) +COPY proxy/go.mod proxy/go.sum ./ +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download -# Copy Go source code +# Copy Go source code and build COPY proxy/ ./ -# Build with CGO enabled for SQLite support -RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o /app/bin/proxy cmd/proxy/main.go +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o /app/bin/proxy cmd/proxy/main.go -# Stage 2: Build Node.js Frontend -FROM node:20-alpine AS node-builder +# ============================================================================ +# Stage: svelte-deps — install SvelteKit dependencies (cached) +# ============================================================================ +FROM node:20-alpine AS svelte-deps + +WORKDIR /app/svelte +COPY svelte/package*.json ./ +RUN --mount=type=cache,target=/root/.npm \ + npm ci + +# ============================================================================ +# Stage: svelte-builder — build SvelteKit frontend +# ============================================================================ +FROM svelte-deps AS svelte-builder + +COPY svelte/ ./ +# shared/ is referenced by vite.config.ts (../shared/frontend/backend) +COPY shared/ /app/shared/ +RUN npm run build + +# ============================================================================ +# Stage: svelte-prod — production SvelteKit deps only +# ============================================================================ +FROM node:20-alpine AS svelte-prod + +WORKDIR /app/svelte +COPY svelte/package*.json ./ +RUN --mount=type=cache,target=/root/.npm \ + npm ci --omit=dev + +# ============================================================================ +# Stage: dev — development image with hot-reload +# ============================================================================ +# CGO is required for mattn/go-sqlite3. To avoid slow first-build times +# at container start, we pre-build the binary (and warm the build cache) +# during image build. CompileDaemon then does fast incremental rebuilds. +FROM golang:1.26-alpine AS dev + +# Copy Node.js 20 from official image (Alpine's repos ship Node 24 which has +# breaking module-resolution changes that break SvelteKit's virtual modules) +COPY --from=node:20-alpine /usr/local/bin/node /usr/local/bin/node +COPY --from=node:20-alpine /usr/local/lib/node_modules /usr/local/lib/node_modules +RUN ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \ + && ln -sf /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx + +# Install build deps and runtime tools +RUN apk add --no-cache \ + libstdc++ \ + git gcc musl-dev sqlite-dev \ + wget su-exec postgresql-client + +# Install CompileDaemon for Go hot-reload +RUN go install github.com/githubnemo/CompileDaemon@latest WORKDIR /app -# Copy package files -COPY web/package*.json ./web/ -WORKDIR /app/web -RUN npm ci +# Pre-install Go dependencies and do initial build to warm cgo cache +COPY proxy/go.mod proxy/go.sum ./proxy/ +RUN cd proxy && go mod download -# Copy web source code and build -COPY web/ ./ -RUN npm run build +COPY proxy/ ./proxy/ +RUN cd proxy && CGO_ENABLED=1 go build -o /tmp/proxy-bin/proxy cmd/proxy/main.go -# Clean up dev dependencies after build -RUN npm ci --only=production && npm cache clean --force +# Pre-install Node dependencies for svelte (layer cache) +COPY svelte/package*.json ./svelte/ +RUN cd svelte && npm install -# Stage 3: Production Runtime +# Copy the dev entrypoint +COPY docker-entrypoint.dev.sh ./ +RUN chmod +x docker-entrypoint.dev.sh + +ENV PORT=3001 +ENV SVELTE_PORT=5174 +ENV CGO_ENABLED=1 + +EXPOSE 3001 5174 + +ENTRYPOINT ["./docker-entrypoint.dev.sh"] + +# ============================================================================ +# Stage: (default) — production runtime +# ============================================================================ FROM node:20-alpine WORKDIR /app -# Install runtime dependencies -RUN apk add --no-cache sqlite wget - -# Create app user for security -RUN addgroup -g 1001 -S appgroup && \ - adduser -S appuser -u 1001 -G appgroup +# Install runtime dependencies (sqlite for legacy, postgresql-client for healthcheck) +RUN apk add --no-cache sqlite wget su-exec postgresql-client # Copy built Go binary COPY --from=go-builder /app/bin/proxy ./bin/proxy RUN chmod +x ./bin/proxy -# Copy built Remix application -COPY --from=node-builder /app/web/build ./web/build -COPY --from=node-builder /app/web/package*.json ./web/ -COPY --from=node-builder /app/web/node_modules ./web/node_modules +# Copy built SvelteKit application with production deps +COPY --from=svelte-builder /app/svelte/build ./svelte/build +COPY --from=svelte-prod /app/svelte/package*.json ./svelte/ +COPY --from=svelte-prod /app/svelte/node_modules ./svelte/node_modules # Create data directory for SQLite database -RUN mkdir -p /app/data && chown -R appuser:appgroup /app +RUN mkdir -p /app/data && chown -R node:node /app # Copy startup script COPY docker-entrypoint.sh ./ @@ -66,7 +134,7 @@ RUN chmod +x docker-entrypoint.sh # Environment variables with defaults ENV PORT=3001 -ENV WEB_PORT=5173 +ENV SVELTE_PORT=5174 ENV READ_TIMEOUT=600 ENV WRITE_TIMEOUT=600 ENV IDLE_TIMEOUT=600 @@ -75,15 +143,11 @@ ENV ANTHROPIC_VERSION=2023-06-01 ENV ANTHROPIC_MAX_RETRIES=3 ENV DB_PATH=/app/data/requests.db -# Expose ports -EXPOSE 3001 5173 +EXPOSE 3001 5174 -# Switch to app user -USER appuser - -# Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget -qO- http://localhost:3001/health > /dev/null || exit 1 -# Start both services -CMD ["./docker-entrypoint.sh"] \ No newline at end of file +# Entrypoint handles privilege drop — compose overrides user to root at start +ENTRYPOINT ["./docker-entrypoint.sh"] +CMD [] diff --git a/Makefile b/Makefile index e4d75c5..5ed236a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build run clean install dev +.PHONY: all build run clean install dev test test-proxy test-proxy-postgres-up test-proxy-postgres-down test-proxy-postgres-contract test-proxy-postgres # Default target all: install build @@ -8,18 +8,18 @@ install: @echo "📦 Installing Go dependencies..." cd proxy && go mod download @echo "📦 Installing Node dependencies..." - cd web && npm install + cd svelte && npm install # Build both services -build: build-proxy build-web +build: build-proxy build-svelte build-proxy: @echo "🔨 Building proxy server..." cd proxy && go build -o ../bin/proxy cmd/proxy/main.go -build-web: - @echo "🔨 Building web interface..." - cd web && npm run build +build-svelte: + @echo "🔨 Building svelte interface..." + cd svelte && npm run build # Run in development mode dev: @@ -30,16 +30,40 @@ dev: run-proxy: cd proxy && go run cmd/proxy/main.go +# Run Go tests for the proxy +test-proxy: + cd proxy && go test ./... + +# Run only the env-gated Postgres contract suite +test-proxy-postgres-contract: + cd proxy && go test ./internal/service -run TestPostgresStorageContract -count=1 + +# Start disposable Postgres for contract tests +test-proxy-postgres-up: + docker compose -f ../docker-compose.test.yml up -d --wait --remove-orphans postgres-test + +# Stop disposable Postgres for contract tests +test-proxy-postgres-down: + docker compose -f ../docker-compose.test.yml down -v --remove-orphans + +# Start disposable Postgres, run contract suite, then stop it +test-proxy-postgres: + @set -e; \ + trap 'docker compose -f ../docker-compose.test.yml down -v --remove-orphans >/dev/null 2>&1 || true' EXIT; \ + docker compose -f ../docker-compose.test.yml up -d --wait --remove-orphans postgres-test; \ + TEST_POSTGRES_DSN=$${TEST_POSTGRES_DSN:-postgresql://$${TEST_POSTGRES_USER:-test}:$${TEST_POSTGRES_PASSWORD:-test}@localhost:$${TEST_POSTGRES_PORT:-5434}/$${TEST_POSTGRES_DB:-claude_code_proxy_test}?sslmode=disable} \ + $(MAKE) test-proxy-postgres-contract + # Run web only -run-web: - cd web && npm run dev +run-svelte: + cd svelte && npm run dev # Clean build artifacts clean: @echo "🧹 Cleaning build artifacts..." rm -rf bin/ - rm -rf web/build/ - rm -rf web/.cache/ + rm -rf svelte/build/ + rm -rf svelte/.svelte-kit/ rm -f requests.db rm -rf requests/ @@ -51,12 +75,17 @@ db-reset: # Help help: - @echo "Claude Code Monitor - Available targets:" + @echo "Claude Code Proxy - Available targets:" @echo " make install - Install all dependencies" @echo " make build - Build both services" @echo " make dev - Run in development mode" @echo " make run-proxy - Run proxy server only" - @echo " make run-web - Run web interface only" + @echo " make test-proxy - Run all Go proxy tests" + @echo " make test-proxy-postgres-up - Start disposable Postgres for contract tests" + @echo " make test-proxy-postgres-down - Stop disposable Postgres for contract tests" + @echo " make test-proxy-postgres-contract - Run Postgres storage contract test (requires TEST_POSTGRES_DSN)" + @echo " make test-proxy-postgres - Start disposable Postgres, run contract test, stop it" + @echo " make run-svelte - Run svelte interface only" @echo " make clean - Clean build artifacts" @echo " make db-reset - Reset database" @echo " make help - Show this help message" diff --git a/README.md b/README.md index 4fcdf22..47b523c 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,11 @@ Claude Code Proxy serves three main purposes: ## Security Defaults -- The proxy binds to `127.0.0.1` by default for local-only access. -- CORS defaults are restricted to localhost origins. -- If you want to expose the proxy on a public interface, you must set `AUTH_ENABLED=true` and provide `AUTH_TOKEN`. +- The proxy currently defaults to `0.0.0.0:3001`, but startup validation refuses non-loopback binds unless you either set `AUTH_ENABLED=true` with `AUTH_TOKEN` or explicitly opt into `TRUST_PROXY=true` for reverse-proxy deployments. +- CORS is configurable and currently defaults to permissive values unless you override it. +- If you expose the proxy directly on a public interface, enable auth and provide a token. - When auth is enabled, the proxy accepts either `Authorization: Bearer ` or `X-API-Key: `. +- Dashboard routes can be protected separately with `DASHBOARD_PASSWORD`, which enables HTTP basic auth for the web UI and dashboard data endpoints. ## Quick Start @@ -86,7 +87,7 @@ Claude Code Proxy serves three main purposes: docker run claude-code-proxy # Run with published ports - docker run -p 3001:3001 -p 5173:5173 \ + docker run -p 3001:3001 -p 5174:5174 \ -e SERVER_HOST=0.0.0.0 \ -e AUTH_ENABLED=true \ -e AUTH_TOKEN=change-me \ @@ -101,13 +102,13 @@ Claude Code Proxy serves three main purposes: # Option 1: Run with config file (recommended) # If you expose the container with `-p`, set server.host to 0.0.0.0 # and enable auth in the mounted config file. - docker run -p 3001:3001 -p 5173:5173 \ + docker run -p 3001:3001 -p 5174:5174 \ -v ./data:/app/data \ -v ./config.yaml:/app/config.yaml:ro \ claude-code-proxy # Option 2: Run with environment variables - docker run -p 3001:3001 -p 5173:5173 \ + docker run -p 3001:3001 -p 5174:5174 \ -v ./data:/app/data \ -e SERVER_HOST=0.0.0.0 \ -e ANTHROPIC_FORWARD_URL=https://api.anthropic.com \ @@ -126,7 +127,7 @@ Claude Code Proxy serves three main purposes: build: . ports: - "3001:3001" - - "5173:5173" + - "5174:5174" volumes: - ./data:/app/data - ./config.yaml:/app/config.yaml:ro # Mount config file @@ -153,7 +154,7 @@ Then launch Claude Code using the `claude` command. This will route Claude Code's requests through the proxy for monitoring. ### Access Points -- **Web Dashboard**: http://localhost:5173 +- **Web Dashboard**: http://localhost:5174 - **API Proxy**: http://localhost:3001 - **Health Check**: http://localhost:3001/health @@ -167,8 +168,8 @@ If you need to run services independently: # Run proxy only make run-proxy -# Run web interface only (in another terminal) -make run-web +# Run Svelte dashboard only (in another terminal) +make run-svelte ``` ### Available Make Commands @@ -177,11 +178,71 @@ make run-web make install # Install all dependencies make build # Build both services make dev # Run in development mode +make test-proxy # Run Go proxy tests make clean # Clean build artifacts make db-reset # Reset database make help # Show all commands ``` +### Running Regression Tests + +The proxy test suite lives under `build/proxy`: + +```bash +cd build/proxy +go test ./... +``` + +Or from the `build/` directory: + +```bash +make test-proxy +``` + +### Running Postgres Storage Contract Tests + +The storage layer has a backend-agnostic contract suite. SQLite runs in the normal Go test path, and PostgreSQL can be exercised by setting `TEST_POSTGRES_DSN`: + +```bash +cd build/proxy +TEST_POSTGRES_DSN='postgresql://user:password@localhost:5432/dbname?sslmode=disable' \ + go test ./internal/service -run TestPostgresStorageContract -count=1 +``` + +Or from `build/`: + +```bash +TEST_POSTGRES_DSN='postgresql://user:password@localhost:5432/dbname?sslmode=disable' \ + make test-proxy-postgres-contract +``` + +The test resets the `requests` and `settings` tables between runs, so point it at a disposable database. + +### Disposable Postgres Test Database + +The repo also includes a dedicated Compose file for contract tests: + +```bash +cd build +make test-proxy-postgres +``` + +That target: +- starts `../docker-compose.test.yml` +- points `TEST_POSTGRES_DSN` at the disposable database by default +- runs `TestPostgresStorageContract` +- tears the database down automatically +- removes orphaned test-compose containers for a clean rerun + +If you want to manage the database lifecycle yourself: + +```bash +cd build +make test-proxy-postgres-up +make test-proxy-postgres-contract +make test-proxy-postgres-down +``` + ## Configuration ### Basic Setup @@ -207,6 +268,8 @@ auth: token: "" ``` +If you set `server.host` to a non-loopback address such as `0.0.0.0`, the proxy will refuse to start unless you also enable auth or explicitly set `TRUST_PROXY=true` for a reverse-proxy deployment. + ### Auth To expose the proxy beyond localhost, enable auth and provide a token: @@ -289,12 +352,17 @@ Use case: Different specialists for different tasks, optimizing for speed/cost/q Override config via environment: - `PORT` - Server port - `SERVER_HOST` - Server bind host +- `TRUST_PROXY` - Skip direct-bind auth enforcement when running behind Traefik or another reverse proxy - `AUTH_ENABLED` - Enable auth for non-health endpoints - `AUTH_TOKEN` - Shared auth secret - `AUTH_API_KEY_HEADER` - Header name for API key auth - `AUTH_ALLOW_LOCALHOST_BYPASS` - Allow localhost requests to bypass auth +- `DASHBOARD_PASSWORD` - Protect the dashboard and dashboard data APIs with HTTP basic auth - `OPENAI_API_KEY` - OpenAI API key +- `DB_TYPE` - Storage backend (`sqlite` or `postgres`) +- `DATABASE_URL` - PostgreSQL connection string when `DB_TYPE=postgres` - `DB_PATH` - Database path +- `PROXY_PUBLIC_URL` - Public proxy URL shown in dashboard setup instructions - `SUBAGENT_MAPPINGS` - Comma-separated mappings (e.g., `"code-reviewer:gpt-4o,data-analyst:o3"`) ### Docker Environment Variables @@ -303,23 +371,29 @@ All environment variables can be configured when running the Docker container: | Variable | Default | Description | |----------|---------|-------------| -| `SERVER_HOST` | `127.0.0.1` | Proxy bind host | +| `SERVER_HOST` | `0.0.0.0` | Proxy bind host | | `PORT` | `3001` | Proxy server port | +| `SVELTE_PORT` | `5174` | Dashboard server port | | `READ_TIMEOUT` | `600` | Server read timeout (seconds) | | `WRITE_TIMEOUT` | `600` | Server write timeout (seconds) | | `IDLE_TIMEOUT` | `600` | Server idle timeout (seconds) | | `ANTHROPIC_FORWARD_URL` | `https://api.anthropic.com` | Target Anthropic API URL | | `ANTHROPIC_VERSION` | `2023-06-01` | Anthropic API version | | `ANTHROPIC_MAX_RETRIES` | `3` | Maximum retry attempts | +| `TRUST_PROXY` | `false` | Allow reverse-proxy deployments without direct auth on the Go bind | | `AUTH_ENABLED` | `false` | Enable auth for non-health endpoints | | `AUTH_TOKEN` | `""` | Shared auth token | | `AUTH_API_KEY_HEADER` | `x-api-key` | Header name for API-key style auth | | `AUTH_ALLOW_LOCALHOST_BYPASS` | `true` | Allow loopback requests to bypass auth | +| `DASHBOARD_PASSWORD` | `""` | HTTP basic auth password for the dashboard | +| `DB_TYPE` | `sqlite` | Storage backend | +| `DATABASE_URL` | `""` | PostgreSQL connection string | | `DB_PATH` | `/app/data/requests.db` | SQLite database path | +| `PROXY_PUBLIC_URL` | `""` | Public proxy URL shown by the Svelte dashboard | Example with custom configuration: ```bash -docker run -p 3001:3001 -p 5173:5173 \ +docker run -p 3001:3001 -p 5174:5174 \ -v ./data:/app/data \ -e SERVER_HOST=0.0.0.0 \ -e AUTH_ENABLED=true \ @@ -338,9 +412,10 @@ claude-code-proxy/ │ ├── cmd/ # Application entry points │ ├── internal/ # Internal packages │ └── go.mod # Go dependencies -├── web/ # React Remix frontend -│ ├── app/ # Remix application +├── svelte/ # SvelteKit dashboard +│ ├── src/ # Svelte application │ └── package.json # Node dependencies +├── shared/ # Shared TypeScript modules used by the dashboard ├── run.sh # Start script ├── .env.example # Environment template └── README.md # This file diff --git a/config.yaml.example b/config.yaml.example index bd820e6..e1b5d20 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -5,7 +5,9 @@ # Server configuration server: # Bind host for the proxy server. - # Defaults to 127.0.0.1 for local-only access. + # Example local-only value. The current built-in default is 0.0.0.0, but + # startup validation rejects public binds unless auth is enabled or + # TRUST_PROXY=true is set for a reverse-proxy deployment. host: 127.0.0.1 # Port to listen on (default: 3001) @@ -53,13 +55,14 @@ providers: # CORS Configuration # Controls Cross-Origin Resource Sharing for the web UI cors: - # Allowed origins. Defaults are localhost-only. + # Allowed origins. The built-in defaults are permissive, so set these + # explicitly if you want tighter browser access. # Can also be set via CORS_ALLOWED_ORIGINS environment variable (comma-separated) allowed_origins: - "http://localhost:3000" - "http://127.0.0.1:3000" - - "http://localhost:5173" - - "http://127.0.0.1:5173" + - "http://localhost:5174" + - "http://127.0.0.1:5174" # Allowed HTTP methods # Can also be set via CORS_ALLOWED_METHODS environment variable (comma-separated) @@ -96,11 +99,25 @@ auth: # Allow requests from localhost to bypass auth when enabled allow_localhost_bypass: true + # Optional dashboard-only password. When set, the Svelte dashboard and + # dashboard data endpoints require HTTP basic auth with username "admin". + dashboard_password: "" + + # Set to true when running behind a trusted reverse proxy and you want to + # skip the public-bind auth requirement enforced at startup. + trust_proxy: false + # Storage configuration storage: + # Storage backend. Supported values: sqlite, postgres + db_type: "sqlite" + # SQLite database path for storing request history db_path: "requests.db" + # PostgreSQL connection string used when db_type=postgres + database_url: "" + # Keep request bodies in storage. Disable for metadata-only tracking. capture_request_body: true @@ -172,8 +189,12 @@ subagents: # AUTH_TOKEN - Shared secret for bearer / API-key auth # AUTH_API_KEY_HEADER - Header name for API-key style auth # AUTH_ALLOW_LOCALHOST_BYPASS - Allow loopback requests to bypass auth (true/false) +# DASHBOARD_PASSWORD - Dashboard HTTP basic auth password +# TRUST_PROXY - Skip public-bind auth enforcement behind a reverse proxy # # Storage: +# DB_TYPE - Storage backend (sqlite/postgres) +# DATABASE_URL - PostgreSQL connection string # DB_PATH - Database file path # STORAGE_CAPTURE_REQUEST_BODY - Keep request bodies (true/false) # STORAGE_CAPTURE_RESPONSE_BODY - Keep response bodies (true/false) diff --git a/docker-entrypoint.dev.sh b/docker-entrypoint.dev.sh new file mode 100755 index 0000000..bd7baa0 --- /dev/null +++ b/docker-entrypoint.dev.sh @@ -0,0 +1,86 @@ +#!/bin/sh + +# Dev entrypoint - runs all services with hot-reload + +set -e + +PUID=${PUID:-1000} +PGID=${PGID:-1000} + +if [ "$(id -u)" = "0" ]; then + if [ "$PUID" != "1000" ] || [ "$PGID" != "1000" ]; then + deluser node 2>/dev/null || true + delgroup node 2>/dev/null || true + addgroup -g "$PGID" -S node + adduser -S -u "$PUID" -G node -h /home/node -s /bin/sh node + fi + + # Fix data dir ownership + chown "$PUID:$PGID" /app/data 2>/dev/null || true + chown "$PUID:$PGID" /app/data/requests.db* 2>/dev/null || true + + # Install/update deps as root (named volumes are root-owned) + cd /app/proxy && go mod download + cd /app/svelte && npm install --loglevel=warn 2>&1 || true + cd /app + + # Fix ownership on everything the node user needs to write to + chown -R "$PUID:$PGID" /app/svelte/node_modules 2>/dev/null || true + # Pre-create .svelte-kit and fix ownership so vite dev can write type definitions + mkdir -p /app/svelte/.svelte-kit/types + chown -R "$PUID:$PGID" /app/svelte/.svelte-kit /app/svelte/build 2>/dev/null || true + + # Ensure Go cache/tmp dirs are writable by the node user + # Use a dedicated dir for the proxy binary so CompileDaemon can overwrite it + mkdir -p /home/node/.cache/go-build /tmp/go-build /tmp/proxy-bin + chown -R "$PUID:$PGID" /home/node/.cache /tmp/go-build /tmp/proxy-bin /go/pkg 2>/dev/null || true + + # Re-exec this script as the node user + exec su-exec "$PUID:$PGID" "$0" "$@" +fi + +echo "=== DEV MODE (uid=$(id -u)) ===" +echo "Proxy Server: http://0.0.0.0:${PORT} (CompileDaemon hot-reload)" +echo "Svelte Dashboard: http://0.0.0.0:${SVELTE_PORT} (vite HMR)" +echo "================" + +cleanup() { + echo "Shutting down dev services..." + kill $PROXY_PID $SVELTE_PID 2>/dev/null || true + exit 0 +} + +trap cleanup SIGTERM SIGINT + +# Start Go proxy with CompileDaemon for hot-reload +cd /app/proxy +HOME=/home/node CompileDaemon -build="go build -o /tmp/proxy-bin/proxy cmd/proxy/main.go" -command="/tmp/proxy-bin/proxy" -graceful-kill=true -graceful-timeout=10 -pattern="(.+\\.go|.+\\.yaml)$" & +PROXY_PID=$! +cd /app + +# Wait for Go proxy to be ready (up to 120s for first compile) +echo "Waiting for proxy to be ready..." +i=0 +while [ $i -lt 120 ]; do + if wget -qO /dev/null "http://localhost:${PORT}/health" 2>/dev/null; then + echo "Proxy is ready!" + break + fi + sleep 2 + i=$((i + 2)) +done +if [ $i -ge 120 ]; then + echo "Warning: proxy not ready after 120s, starting frontends anyway" +fi + +# Start SvelteKit dev server (Vite HMR) +cd /app/svelte +PORT=${SVELTE_PORT} HOST=0.0.0.0 DASHBOARD_PASSWORD="${DASHBOARD_PASSWORD}" npm run dev -- --host 0.0.0.0 --port ${SVELTE_PORT} & +SVELTE_PID=$! +cd /app + +echo "" +echo "All dev services started. Watching for file changes..." +echo "" + +wait diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh old mode 100644 new mode 100755 index 1ea9e68..c2dd30b --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,10 +1,30 @@ #!/bin/sh # Docker entrypoint script for Claude Code Proxy -# Starts both the Go proxy server and Remix frontend +# Starts both the Go proxy server and SvelteKit frontend set -e +# Support user-supplied UID/GID via PUID/PGID env vars (default: 1000). +PUID=${PUID:-1000} +PGID=${PGID:-1000} + +# When running as root, fix data dir ownership and drop to the target user. +if [ "$(id -u)" = "0" ]; then + # Update the node user/group to match requested UID/GID + if [ "$PUID" != "1000" ] || [ "$PGID" != "1000" ]; then + deluser node 2>/dev/null || true + delgroup node 2>/dev/null || true + addgroup -g "$PGID" -S node + adduser -S -u "$PUID" -G node -h /home/node -s /bin/sh node + fi + + # Fix ownership of data dir and sqlite files (not postgres subdir) + chown "$PUID:$PGID" /app/data 2>/dev/null || true + chown "$PUID:$PGID" /app/data/requests.db* 2>/dev/null || true + exec su-exec node "$0" "$@" +fi + echo "🚀 Starting Claude Code Proxy services..." echo "=========================================" @@ -12,7 +32,7 @@ echo "=========================================" cleanup() { echo "" echo "🛑 Shutting down services..." - kill $PROXY_PID $WEB_PID 2>/dev/null || true + kill $PROXY_PID $SVELTE_PID 2>/dev/null || true exit 0 } @@ -21,8 +41,13 @@ trap cleanup SIGTERM SIGINT echo "📊 Configuration:" echo " - Proxy Server: http://0.0.0.0:${PORT}" -echo " - Web Dashboard: http://0.0.0.0:${WEB_PORT}" +echo " - Svelte Dashboard: http://0.0.0.0:${SVELTE_PORT}" +echo " - Database type: ${DB_TYPE:-sqlite}" +if [ "${DB_TYPE}" = "postgres" ] || [ "${DB_TYPE}" = "postgresql" ]; then +echo " - Database: PostgreSQL (via DATABASE_URL)" +else echo " - Database: ${DB_PATH}" +fi echo " - Anthropic API: ${ANTHROPIC_FORWARD_URL}" echo "=========================================" @@ -35,24 +60,38 @@ IDLE_TIMEOUT=${IDLE_TIMEOUT}s \ ANTHROPIC_FORWARD_URL=${ANTHROPIC_FORWARD_URL} \ ANTHROPIC_VERSION=${ANTHROPIC_VERSION} \ ANTHROPIC_MAX_RETRIES=${ANTHROPIC_MAX_RETRIES} \ +DB_TYPE=${DB_TYPE:-sqlite} \ DB_PATH=${DB_PATH} \ +DATABASE_URL=${DATABASE_URL:-} \ ./bin/proxy & PROXY_PID=$! -# Wait for proxy to start -sleep 3 +# Wait for proxy to be ready +echo "⏳ Waiting for proxy to be ready..." +i=0 +while [ $i -lt 30 ]; do + if wget -qO /dev/null "http://localhost:${PORT}/health" 2>/dev/null; then + echo "✅ Proxy is ready!" + break + fi + sleep 1 + i=$((i + 1)) +done +if [ $i -ge 30 ]; then + echo "⚠️ Warning: proxy not ready after 30s, starting frontends anyway" +fi -# Start web server -echo "🔄 Starting web server..." -cd web -PORT=${WEB_PORT} HOST=0.0.0.0 NODE_ENV=production npx remix-serve build/server/index.js & -WEB_PID=$! +# Start SvelteKit server +echo "🔄 Starting SvelteKit server..." +cd svelte +PORT=${SVELTE_PORT} HOST=0.0.0.0 NODE_ENV=production DASHBOARD_PASSWORD="${DASHBOARD_PASSWORD}" node build/index.js & +SVELTE_PID=$! cd .. echo "" echo "✨ All services started successfully!" echo "=========================================" -echo "📊 Web Dashboard: http://localhost:${WEB_PORT}" +echo "📊 Web Dashboard: http://localhost:${SVELTE_PORT}" echo "🔌 API Proxy: http://localhost:${PORT}" echo "💚 Health Check: http://localhost:${PORT}/health" echo "=========================================" diff --git a/proxy/cmd/proxy/main.go b/proxy/cmd/proxy/main.go index 698b009..9ffbf95 100644 --- a/proxy/cmd/proxy/main.go +++ b/proxy/cmd/proxy/main.go @@ -17,6 +17,7 @@ import ( "github.com/seifghazi/claude-code-monitor/internal/handler" "github.com/seifghazi/claude-code-monitor/internal/middleware" "github.com/seifghazi/claude-code-monitor/internal/provider" + "github.com/seifghazi/claude-code-monitor/internal/runtime" "github.com/seifghazi/claude-code-monitor/internal/service" ) @@ -36,65 +37,45 @@ func main() { // Initialize model router modelRouter := service.NewModelRouter(cfg, providers, logger) - // Use legacy anthropic service for backward compatibility - anthropicService := service.NewAnthropicService(&cfg.Anthropic) - - // Use SQLite storage - storageService, err := service.NewSQLiteStorageService(&cfg.Storage) - if err != nil { - logger.Fatalf("❌ Failed to initialize SQLite storage: %v", err) + // Initialize storage based on DB_TYPE + var storageService service.StorageService + switch cfg.Storage.DBType { + case "postgres", "postgresql": + if cfg.Storage.DatabaseURL == "" { + logger.Fatalf("❌ DATABASE_URL is required when DB_TYPE=postgres") + } + storageService, err = service.NewPostgresStorageService(&cfg.Storage) + if err != nil { + logger.Fatalf("❌ Failed to initialize PostgreSQL storage: %v", err) + } + logger.Println("🐘 PostgreSQL database ready") + default: + storageService, err = service.NewSQLiteStorageService(&cfg.Storage) + if err != nil { + logger.Fatalf("❌ Failed to initialize SQLite storage: %v", err) + } + logger.Println("🗿 SQLite database ready") } - logger.Println("🗿 SQLite database ready") - h := handler.New(anthropicService, storageService, logger, modelRouter) - - r := mux.NewRouter() - - corsHandler := handlers.CORS( - handlers.AllowedOrigins(cfg.CORS.AllowedOrigins), - handlers.AllowedMethods(cfg.CORS.AllowedMethods), - handlers.AllowedHeaders(cfg.CORS.AllowedHeaders), - ) - - r.Use(middleware.Logging) - r.Use(middleware.Auth(cfg.Auth)) - - r.HandleFunc("/v1/chat/completions", h.ChatCompletions).Methods("POST") - r.HandleFunc("/v1/messages", h.Messages).Methods("POST") - r.HandleFunc("/v1/models", h.Models).Methods("GET") - r.HandleFunc("/health", h.Health).Methods("GET") - - r.HandleFunc("/", h.UI).Methods("GET") - r.HandleFunc("/ui", h.UI).Methods("GET") - r.HandleFunc("/api/requests", h.GetRequests).Methods("GET") - r.HandleFunc("/api/requests", h.DeleteRequests).Methods("DELETE") - r.HandleFunc("/api/conversations", h.GetConversations).Methods("GET") - r.HandleFunc("/api/conversations/{id}", h.GetConversationByID).Methods("GET") - r.HandleFunc("/api/conversations/project", h.GetConversationsByProject).Methods("GET") - - r.NotFoundHandler = http.HandlerFunc(h.NotFound) + h := handler.New(storageService, logger, modelRouter, providers["anthropic"], cfg.Providers.Anthropic.DemoteNonstreaming) srv := &http.Server{ Addr: net.JoinHostPort(cfg.Server.Host, cfg.Server.Port), - Handler: corsHandler(r), + Handler: buildHandler(cfg, h), ReadTimeout: cfg.Server.ReadTimeout, WriteTimeout: cfg.Server.WriteTimeout, IdleTimeout: cfg.Server.IdleTimeout, } go func() { - logger.Printf("🚀 Claude Code Monitor Server running on http://%s", srv.Addr) - logger.Printf("📡 API endpoints available at:") - logger.Printf(" - POST http://%s/v1/messages (Anthropic format)", srv.Addr) - logger.Printf(" - GET http://%s/v1/models", srv.Addr) - logger.Printf(" - GET http://%s/health", srv.Addr) - logger.Printf("🎨 Web UI available at:") - logger.Printf(" - GET http://%s/ (Request Visualizer)", srv.Addr) - logger.Printf(" - GET http://%s/api/requests (Request API)", srv.Addr) + logger.Println("🚀 Claude Code Proxy started") + logger.Printf(" upstream: %s", cfg.Providers.Anthropic.BaseURL) + logger.Printf(" storage: %s", cfg.Storage.DBType) if cfg.Auth.Enabled { - logger.Printf("🔐 Auth enabled using bearer token or %s", cfg.Auth.APIKeyHeader) - } else { - logger.Printf("🔓 Auth disabled for local-only access") + logger.Printf(" auth: bearer / %s", cfg.Auth.APIKeyHeader) + } + if cfg.Auth.DashboardPassword != "" { + logger.Printf(" dashboard: password protected") } if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { @@ -104,10 +85,41 @@ func main() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit + sig := <-quit - logger.Println("🛑 Shutting down server...") + // Drain ceiling: must accommodate the 30-min forward context in + // handlers.Messages plus a small buffer. Container-level + // stop_grace_period must be set just above this in docker-compose so + // docker doesn't SIGKILL us mid-drain. + const drainCeiling = 32 * time.Minute + const drainPoll = 1 * time.Second + logger.Printf("🛑 Received %s — draining (ceiling %s)...", sig, drainCeiling) + runtime.SetDraining(true) + + drainStart := time.Now() + lastLog := drainStart + for { + n := runtime.InFlight() + if n == 0 { + logger.Printf("✅ Drain complete (%s elapsed, in-flight=0)", time.Since(drainStart).Round(time.Second)) + break + } + if time.Since(drainStart) >= drainCeiling { + logger.Printf("⏰ Drain ceiling hit — proceeding with shutdown (in_flight=%d)", n) + break + } + if time.Since(lastLog) >= 10*time.Second { + logger.Printf("⏳ Draining: in_flight=%d elapsed=%s", n, time.Since(drainStart).Round(time.Second)) + lastLog = time.Now() + } + time.Sleep(drainPoll) + } + + // Server.Shutdown stops accepting new connections and waits for active + // handlers to return. Use a short ceiling here — the long wait already + // happened in the drain loop above. If a handler is somehow still + // running past the drain ceiling, give it 30s before forced close. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -123,3 +135,57 @@ func main() { logger.Println("✅ Server exited") } + +func buildHandler(cfg *config.Config, h *handler.Handler) http.Handler { + r := mux.NewRouter() + + corsHandler := handlers.CORS( + handlers.AllowedOrigins(cfg.CORS.AllowedOrigins), + handlers.AllowedMethods(cfg.CORS.AllowedMethods), + handlers.AllowedHeaders(cfg.CORS.AllowedHeaders), + ) + + r.Use(middleware.Logging) + r.Use(middleware.Auth(cfg.Auth)) + + // Liveness/readiness — must remain unauthenticated and uncounted. + r.HandleFunc("/health", h.Health).Methods("GET") + r.HandleFunc("/livez", h.Livez).Methods("GET") + r.HandleFunc("/openapi.json", h.OpenAPIJSON).Methods("GET") + r.HandleFunc("/openapi.yaml", h.OpenAPIYAML).Methods("GET") + + // Proxy endpoints — no dashboard auth, but tracked by the in-flight + // gauge so graceful shutdown can wait for them to drain. + // Trailing slash on the prefix keeps the boundary tight: /v10 etc. won't match. + v1 := r.PathPrefix("/v1/").Subrouter() + v1.Use(middleware.InFlight) + v1.HandleFunc("/chat/completions", h.ChatCompletions).Methods("POST") + v1.HandleFunc("/messages", h.Messages).Methods("POST") + // Catch-all: proxy any other /v1/* requests to Anthropic (config, quota, batches, etc.) + v1.PathPrefix("/").HandlerFunc(h.ProxyPassthrough) + + // Dashboard/data endpoints — protected by DASHBOARD_PASSWORD basic auth + dashboard := r.PathPrefix("/").Subrouter() + dashboard.Use(middleware.DashboardAuth(cfg.Auth.DashboardPassword)) + dashboard.HandleFunc("/", h.UI).Methods("GET") + dashboard.HandleFunc("/ui", h.UI).Methods("GET") + dashboard.HandleFunc("/api/requests", h.GetRequests).Methods("GET") + dashboard.HandleFunc("/api/requests", h.DeleteRequests).Methods("DELETE") + dashboard.HandleFunc("/api/requests/summary", h.GetRequestsSummary).Methods("GET") + dashboard.HandleFunc("/api/requests/latest-date", h.GetLatestRequestDate).Methods("GET") + dashboard.HandleFunc("/api/requests/{id}", h.GetRequestByID).Methods("GET") + dashboard.HandleFunc("/api/stats", h.GetStats).Methods("GET") + dashboard.HandleFunc("/api/stats/dashboard", h.GetDashboardStats).Methods("GET") + dashboard.HandleFunc("/api/stats/hourly", h.GetHourlyStats).Methods("GET") + dashboard.HandleFunc("/api/stats/models", h.GetModelStats).Methods("GET") + dashboard.HandleFunc("/api/stats/organizations", h.GetOrganizations).Methods("GET") + dashboard.HandleFunc("/api/conversations", h.GetConversations).Methods("GET") + dashboard.HandleFunc("/api/conversations/{id}", h.GetConversationByID).Methods("GET") + dashboard.HandleFunc("/api/conversations/project", h.GetConversationsByProject).Methods("GET") + dashboard.HandleFunc("/api/settings", h.GetSettings).Methods("GET") + dashboard.HandleFunc("/api/settings", h.SaveSettings).Methods("PUT") + + r.NotFoundHandler = http.HandlerFunc(h.NotFound) + + return corsHandler(r) +} diff --git a/proxy/cmd/proxy/main_test.go b/proxy/cmd/proxy/main_test.go new file mode 100644 index 0000000..8fca210 --- /dev/null +++ b/proxy/cmd/proxy/main_test.go @@ -0,0 +1,193 @@ +package main + +import ( + "io" + "log" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/seifghazi/claude-code-monitor/internal/config" + httphandler "github.com/seifghazi/claude-code-monitor/internal/handler" + "github.com/seifghazi/claude-code-monitor/internal/model" + "github.com/seifghazi/claude-code-monitor/internal/service" +) + +type routerStorageStub struct { + getUsageStatsFn func(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) +} + +func (s *routerStorageStub) SaveRequest(*model.RequestLog) (string, error) { panic("unexpected call") } +func (s *routerStorageStub) GetRequests(int, int, string) ([]model.RequestLog, int, error) { + panic("unexpected call") +} +func (s *routerStorageStub) GetAllRequests(string) ([]*model.RequestLog, error) { + panic("unexpected call") +} +func (s *routerStorageStub) GetRequestByShortID(string) (*model.RequestLog, string, error) { + panic("unexpected call") +} +func (s *routerStorageStub) ClearRequests() (int, error) { panic("unexpected call") } +func (s *routerStorageStub) UpdateRequestWithGrading(string, *model.PromptGrade) error { + panic("unexpected call") +} +func (s *routerStorageStub) UpdateRequestWithResponse(*model.RequestLog) error { + panic("unexpected call") +} +func (s *routerStorageStub) DeleteRequestsOlderThan(time.Duration) (int, error) { + panic("unexpected call") +} +func (s *routerStorageStub) GetDatabaseStats() (map[string]interface{}, error) { + panic("unexpected call") +} +func (s *routerStorageStub) GetUsageStats(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) { + if s.getUsageStatsFn != nil { + return s.getUsageStatsFn(startDate, endDate, modelFilter, orgFilter) + } + return &model.UsageStats{}, nil +} +func (s *routerStorageStub) GetRequestsSummary(string) ([]*model.RequestSummary, error) { + panic("unexpected call") +} +func (s *routerStorageStub) GetRequestsSummaryPaginated(string, string, string, int, int) ([]*model.RequestSummary, int, error) { + panic("unexpected call") +} +func (s *routerStorageStub) GetStats(string, string, string) (*model.DashboardStats, error) { + panic("unexpected call") +} +func (s *routerStorageStub) GetHourlyStats(string, string, int, string) (*model.HourlyStatsResponse, error) { + panic("unexpected call") +} +func (s *routerStorageStub) GetModelStats(string, string, string) (*model.ModelStatsResponse, error) { + panic("unexpected call") +} +func (s *routerStorageStub) GetLatestRequestDate() (*time.Time, error) { panic("unexpected call") } +func (s *routerStorageStub) GetDistinctOrganizations() ([]string, error) { + panic("unexpected call") +} +func (s *routerStorageStub) GetSettings() (*model.ProxySettings, error) { + panic("unexpected call") +} +func (s *routerStorageStub) SaveSettings(*model.ProxySettings) error { panic("unexpected call") } +func (s *routerStorageStub) GetConfig() *config.StorageConfig { return &config.StorageConfig{} } +func (s *routerStorageStub) EnsureDirectoryExists() error { return nil } +func (s *routerStorageStub) Close() error { return nil } + +var _ service.StorageService = (*routerStorageStub)(nil) + +func newRouterUnderTest(t *testing.T, cfg *config.Config, storage service.StorageService) http.Handler { + t.Helper() + + h := httphandler.New(storage, log.New(io.Discard, "", 0), nil, nil, false) + return buildHandler(cfg, h) +} + +func TestBuildHandlerKeepsDiscoveryRoutesPublicWhileProtectingDashboard(t *testing.T) { + cfg := &config.Config{ + Auth: config.AuthConfig{ + Enabled: true, + Token: "proxy-secret", + APIKeyHeader: "X-API-Key", + DashboardPassword: "dashboard-secret", + }, + CORS: config.CORSConfig{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Authorization", "Content-Type", "X-API-Key"}, + }, + } + + router := newRouterUnderTest(t, cfg, &routerStorageStub{ + getUsageStatsFn: func(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) { + return &model.UsageStats{TotalRequests: 11}, nil + }, + }) + + for _, path := range []string{"/health", "/openapi.json", "/openapi.yaml"} { + req := httptest.NewRequest(http.MethodGet, path, nil) + req.RemoteAddr = "10.0.0.5:12345" + rr := httptest.NewRecorder() + + router.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected %s to stay public, got %d", path, rr.Code) + } + } + + req := httptest.NewRequest(http.MethodGet, "/api/stats", nil) + req.RemoteAddr = "10.0.0.5:12345" + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + + if rr.Code != http.StatusUnauthorized { + t.Fatalf("expected dashboard endpoint to require auth, got %d", rr.Code) + } +} + +func TestBuildHandlerRequiresBothProxyAndDashboardCredentialsForDashboardAPI(t *testing.T) { + cfg := &config.Config{ + Auth: config.AuthConfig{ + Enabled: true, + Token: "proxy-secret", + APIKeyHeader: "X-API-Key", + DashboardPassword: "dashboard-secret", + }, + CORS: config.CORSConfig{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Authorization", "Content-Type", "X-API-Key"}, + }, + } + + router := newRouterUnderTest(t, cfg, &routerStorageStub{ + getUsageStatsFn: func(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) { + return &model.UsageStats{TotalRequests: 5}, nil + }, + }) + + testCases := []struct { + name string + setup func(*http.Request) + wantStatus int + }{ + { + name: "proxy auth only", + setup: func(r *http.Request) { + r.Header.Set("X-API-Key", "proxy-secret") + }, + wantStatus: http.StatusUnauthorized, + }, + { + name: "dashboard auth only", + setup: func(r *http.Request) { + r.SetBasicAuth("admin", "dashboard-secret") + }, + wantStatus: http.StatusUnauthorized, + }, + { + name: "both auth layers", + setup: func(r *http.Request) { + r.Header.Set("X-API-Key", "proxy-secret") + r.SetBasicAuth("admin", "dashboard-secret") + }, + wantStatus: http.StatusOK, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/stats", nil) + req.RemoteAddr = "10.0.0.5:12345" + tc.setup(req) + rr := httptest.NewRecorder() + + router.ServeHTTP(rr, req) + + if rr.Code != tc.wantStatus { + t.Fatalf("expected status %d, got %d", tc.wantStatus, rr.Code) + } + }) + } +} diff --git a/proxy/go.mod b/proxy/go.mod index b07fbc3..cf3781f 100644 --- a/proxy/go.mod +++ b/proxy/go.mod @@ -6,6 +6,7 @@ require ( github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.28 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/proxy/go.sum b/proxy/go.sum index e0a833a..c644d99 100644 --- a/proxy/go.sum +++ b/proxy/go.sum @@ -6,6 +6,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/proxy/internal/config/config.go b/proxy/internal/config/config.go index 45d5f79..e88e6e3 100644 --- a/proxy/internal/config/config.go +++ b/proxy/internal/config/config.go @@ -20,7 +20,6 @@ type Config struct { Subagents SubagentsConfig `yaml:"subagents"` Auth AuthConfig `yaml:"auth"` CORS CORSConfig `yaml:"cors"` - Anthropic AnthropicConfig } type CORSConfig struct { @@ -51,9 +50,11 @@ type ProvidersConfig struct { } type AnthropicProviderConfig struct { - BaseURL string `yaml:"base_url"` - Version string `yaml:"version"` - MaxRetries int `yaml:"max_retries"` + BaseURL string `yaml:"base_url"` + Version string `yaml:"version"` + MaxRetries int `yaml:"max_retries"` + ResponseHeaderTimeout time.Duration `yaml:"response_header_timeout"` + DemoteNonstreaming bool `yaml:"demote_nonstreaming"` } type OpenAIProviderConfig struct { @@ -68,17 +69,15 @@ type AuthConfig struct { Token string `yaml:"token"` APIKeyHeader string `yaml:"api_key_header"` AllowLocalhostBypass bool `yaml:"allow_localhost_bypass"` -} - -type AnthropicConfig struct { - BaseURL string - Version string - MaxRetries int + DashboardPassword string `yaml:"dashboard_password"` + TrustProxy bool `yaml:"trust_proxy"` // Skip bind-address auth check (for Docker / reverse-proxy setups) } type StorageConfig struct { RequestsDir string `yaml:"requests_dir"` + DBType string `yaml:"db_type"` DBPath string `yaml:"db_path"` + DatabaseURL string `yaml:"database_url"` CaptureRequestBody bool `yaml:"capture_request_body"` CaptureResponseBody bool `yaml:"capture_response_body"` MetadataOnly bool `yaml:"metadata_only"` @@ -136,6 +135,12 @@ func Load() (*Config, error) { if envRetries := os.Getenv("ANTHROPIC_MAX_RETRIES"); envRetries != "" { cfg.Providers.Anthropic.MaxRetries = getInt("ANTHROPIC_MAX_RETRIES", cfg.Providers.Anthropic.MaxRetries) } + if envTimeout := os.Getenv("ANTHROPIC_RESPONSE_HEADER_TIMEOUT"); envTimeout != "" { + cfg.Providers.Anthropic.ResponseHeaderTimeout = getDuration("ANTHROPIC_RESPONSE_HEADER_TIMEOUT", cfg.Providers.Anthropic.ResponseHeaderTimeout) + } + if os.Getenv("ANTHROPIC_DEMOTE_NONSTREAMING") != "" { + cfg.Providers.Anthropic.DemoteNonstreaming = envBool("ANTHROPIC_DEMOTE_NONSTREAMING") + } // Override OpenAI settings if envURL := os.Getenv("OPENAI_BASE_URL"); envURL != "" { @@ -144,16 +149,16 @@ func Load() (*Config, error) { if envKey := os.Getenv("OPENAI_API_KEY"); envKey != "" { cfg.Providers.OpenAI.APIKey = envKey } - if envAllow := os.Getenv("OPENAI_ALLOW_CLIENT_API_KEY"); envAllow != "" { - cfg.Providers.OpenAI.AllowClientAPIKey = envAllow == "true" || envAllow == "1" + if os.Getenv("OPENAI_ALLOW_CLIENT_API_KEY") != "" { + cfg.Providers.OpenAI.AllowClientAPIKey = envBool("OPENAI_ALLOW_CLIENT_API_KEY") } if envHeader := os.Getenv("OPENAI_CLIENT_API_KEY_HEADER"); envHeader != "" { cfg.Providers.OpenAI.ClientAPIKeyHeader = envHeader } // Override auth settings - if envAuthEnabled := os.Getenv("AUTH_ENABLED"); envAuthEnabled != "" { - cfg.Auth.Enabled = envAuthEnabled == "true" || envAuthEnabled == "1" + if os.Getenv("AUTH_ENABLED") != "" { + cfg.Auth.Enabled = envBool("AUTH_ENABLED") } if envAuthToken := os.Getenv("AUTH_TOKEN"); envAuthToken != "" { cfg.Auth.Token = envAuthToken @@ -161,22 +166,34 @@ func Load() (*Config, error) { if envAPIKeyHeader := os.Getenv("AUTH_API_KEY_HEADER"); envAPIKeyHeader != "" { cfg.Auth.APIKeyHeader = envAPIKeyHeader } - if envLocalBypass := os.Getenv("AUTH_ALLOW_LOCALHOST_BYPASS"); envLocalBypass != "" { - cfg.Auth.AllowLocalhostBypass = envLocalBypass == "true" || envLocalBypass == "1" + if os.Getenv("AUTH_ALLOW_LOCALHOST_BYPASS") != "" { + cfg.Auth.AllowLocalhostBypass = envBool("AUTH_ALLOW_LOCALHOST_BYPASS") + } + if envDashPass := os.Getenv("DASHBOARD_PASSWORD"); envDashPass != "" { + cfg.Auth.DashboardPassword = envDashPass + } + if os.Getenv("TRUST_PROXY") != "" { + cfg.Auth.TrustProxy = envBool("TRUST_PROXY") } // Override storage settings + if envDBType := os.Getenv("DB_TYPE"); envDBType != "" { + cfg.Storage.DBType = envDBType + } if envPath := os.Getenv("DB_PATH"); envPath != "" { cfg.Storage.DBPath = envPath } - if envCaptureReq := os.Getenv("STORAGE_CAPTURE_REQUEST_BODY"); envCaptureReq != "" { - cfg.Storage.CaptureRequestBody = envCaptureReq == "true" || envCaptureReq == "1" + if envDatabaseURL := os.Getenv("DATABASE_URL"); envDatabaseURL != "" { + cfg.Storage.DatabaseURL = envDatabaseURL } - if envCaptureResp := os.Getenv("STORAGE_CAPTURE_RESPONSE_BODY"); envCaptureResp != "" { - cfg.Storage.CaptureResponseBody = envCaptureResp == "true" || envCaptureResp == "1" + if os.Getenv("STORAGE_CAPTURE_REQUEST_BODY") != "" { + cfg.Storage.CaptureRequestBody = envBool("STORAGE_CAPTURE_REQUEST_BODY") } - if envMetadataOnly := os.Getenv("STORAGE_METADATA_ONLY"); envMetadataOnly != "" { - cfg.Storage.MetadataOnly = envMetadataOnly == "true" || envMetadataOnly == "1" + if os.Getenv("STORAGE_CAPTURE_RESPONSE_BODY") != "" { + cfg.Storage.CaptureResponseBody = envBool("STORAGE_CAPTURE_RESPONSE_BODY") + } + if os.Getenv("STORAGE_METADATA_ONLY") != "" { + cfg.Storage.MetadataOnly = envBool("STORAGE_METADATA_ONLY") } if envRetentionDays := os.Getenv("STORAGE_RETENTION_DAYS"); envRetentionDays != "" { cfg.Storage.RetentionDays = getInt("STORAGE_RETENTION_DAYS", cfg.Storage.RetentionDays) @@ -201,13 +218,6 @@ func Load() (*Config, error) { cfg.CORS.AllowedHeaders = splitAndTrim(envHeaders) } - // Sync legacy Anthropic config - cfg.Anthropic = AnthropicConfig{ - BaseURL: cfg.Providers.Anthropic.BaseURL, - Version: cfg.Providers.Anthropic.Version, - MaxRetries: cfg.Providers.Anthropic.MaxRetries, - } - // After loading from file, apply any timeout conversions if needed if cfg.Server.Timeouts.Read != "" { if duration, err := time.ParseDuration(cfg.Server.Timeouts.Read); err == nil { @@ -225,13 +235,6 @@ func Load() (*Config, error) { } } - // Sync legacy Anthropic config with new structure - cfg.Anthropic = AnthropicConfig{ - BaseURL: cfg.Providers.Anthropic.BaseURL, - Version: cfg.Providers.Anthropic.Version, - MaxRetries: cfg.Providers.Anthropic.MaxRetries, - } - if err := validateSecurity(cfg); err != nil { return nil, err } @@ -251,7 +254,7 @@ func (c *Config) loadFromFile(path string) error { func defaultConfig() *Config { return &Config{ Server: ServerConfig{ - Host: "127.0.0.1", + Host: "0.0.0.0", Port: "3001", ReadTimeout: 600 * time.Second, WriteTimeout: 600 * time.Second, @@ -259,9 +262,10 @@ func defaultConfig() *Config { }, Providers: ProvidersConfig{ Anthropic: AnthropicProviderConfig{ - BaseURL: "https://api.anthropic.com", - Version: "2023-06-01", - MaxRetries: 3, + BaseURL: "https://api.anthropic.com", + Version: "2023-06-01", + MaxRetries: 3, + ResponseHeaderTimeout: 300 * time.Second, }, OpenAI: OpenAIProviderConfig{ BaseURL: "https://api.openai.com", @@ -271,6 +275,7 @@ func defaultConfig() *Config { }, }, Storage: StorageConfig{ + DBType: "sqlite", DBPath: "requests.db", CaptureRequestBody: true, CaptureResponseBody: true, @@ -298,12 +303,7 @@ func defaultConfig() *Config { AllowLocalhostBypass: true, }, CORS: CORSConfig{ - AllowedOrigins: []string{ - "http://localhost:3000", - "http://127.0.0.1:3000", - "http://localhost:5173", - "http://127.0.0.1:5173", - }, + AllowedOrigins: []string{"*"}, AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"}, AllowedHeaders: []string{ "Accept", @@ -341,11 +341,17 @@ func candidateConfigPaths() []string { func validateSecurity(cfg *Config) error { if cfg.Server.Host == "" { - cfg.Server.Host = "127.0.0.1" + cfg.Server.Host = "0.0.0.0" + } + + // When behind a reverse proxy (Docker/Traefik), skip the bind-address auth requirement. + // The proxy is not directly exposed; the reverse proxy handles access control. + if cfg.Auth.TrustProxy { + return nil } if !isLoopbackHost(cfg.Server.Host) && !cfg.Auth.Enabled { - return fmt.Errorf("refusing to bind to %q without auth enabled; set AUTH_ENABLED=true and AUTH_TOKEN for public access", cfg.Server.Host) + return fmt.Errorf("refusing to bind to %q without auth enabled; set AUTH_ENABLED=true and AUTH_TOKEN, or TRUST_PROXY=true for reverse-proxy setups", cfg.Server.Host) } if cfg.Auth.Enabled && cfg.Auth.Token == "" && !isLoopbackHost(cfg.Server.Host) { @@ -386,6 +392,11 @@ func loadFirstAvailableConfig(cfg *Config, paths []string) error { return nil } +func envBool(key string) bool { + v := strings.ToLower(os.Getenv(key)) + return v == "true" || v == "1" || v == "yes" +} + func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value diff --git a/proxy/internal/config/config_test.go b/proxy/internal/config/config_test.go index 84e4cfa..fa31e94 100644 --- a/proxy/internal/config/config_test.go +++ b/proxy/internal/config/config_test.go @@ -93,35 +93,30 @@ func TestLoadFirstAvailableConfigSkipsMissingFiles(t *testing.T) { } } -func TestDefaultConfigUsesLoopbackAndLocalCors(t *testing.T) { +func TestDefaultConfigUsesPublicBindAndWildcardCors(t *testing.T) { cfg := defaultConfig() - if cfg.Server.Host != "127.0.0.1" { - t.Fatalf("expected loopback host, got %q", cfg.Server.Host) + if cfg.Server.Host != "0.0.0.0" { + t.Fatalf("expected 0.0.0.0 host, got %q", cfg.Server.Host) } if cfg.Auth.Enabled { - t.Fatal("expected auth to be disabled by default for local development") + t.Fatal("expected auth to be disabled by default") } - if len(cfg.CORS.AllowedOrigins) == 0 { - t.Fatal("expected local CORS origins to be configured") - } - - for _, origin := range cfg.CORS.AllowedOrigins { - if origin == "*" { - t.Fatal("expected wildcard origin to be removed from defaults") - } + if len(cfg.CORS.AllowedOrigins) != 1 || cfg.CORS.AllowedOrigins[0] != "*" { + t.Fatalf("expected default CORS origins to be [*], got %v", cfg.CORS.AllowedOrigins) } } -func TestValidateSecurityRejectsPublicBindWithoutAuth(t *testing.T) { +func TestValidateSecurityRejectsPublicBindWithoutAuthOrTrustProxy(t *testing.T) { cfg := defaultConfig() cfg.Server.Host = "0.0.0.0" cfg.Auth.Enabled = false + cfg.Auth.TrustProxy = false if err := validateSecurity(cfg); err == nil { - t.Fatal("expected validation error for public bind without auth") + t.Fatal("expected validation error for public bind without auth or trust_proxy") } } @@ -135,3 +130,218 @@ func TestValidateSecurityAllowsPublicBindWithAuthToken(t *testing.T) { t.Fatalf("expected public bind with auth token to be allowed, got %v", err) } } + +func TestValidateSecurityAllowsPublicBindWithTrustProxy(t *testing.T) { + cfg := defaultConfig() + cfg.Server.Host = "0.0.0.0" + cfg.Auth.Enabled = false + cfg.Auth.TrustProxy = true + + if err := validateSecurity(cfg); err != nil { + t.Fatalf("expected public bind with trust_proxy to be allowed, got %v", err) + } +} + +func TestEnvBool(t *testing.T) { + tests := []struct { + name string + value string + want bool + }{ + {"true lowercase", "true", true}, + {"true uppercase", "TRUE", true}, + {"true mixed case", "True", true}, + {"one", "1", true}, + {"yes lowercase", "yes", true}, + {"yes uppercase", "YES", true}, + {"false", "false", false}, + {"zero", "0", false}, + {"no", "no", false}, + {"empty", "", false}, + {"random string", "maybe", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key := "TEST_ENV_BOOL_" + tt.name + if tt.value != "" { + os.Setenv(key, tt.value) + defer os.Unsetenv(key) + } else { + os.Unsetenv(key) + } + + got := envBool(key) + if got != tt.want { + t.Errorf("envBool(%q) with value %q = %v, want %v", key, tt.value, got, tt.want) + } + }) + } +} + +func TestCORSEnvOverride(t *testing.T) { + tests := []struct { + name string + envKey string + envVal string + check func(*Config) bool + desc string + }{ + { + name: "origins override", + envKey: "CORS_ALLOWED_ORIGINS", + envVal: "https://example.com, https://other.com", + check: func(c *Config) bool { + return len(c.CORS.AllowedOrigins) == 2 && + c.CORS.AllowedOrigins[0] == "https://example.com" && + c.CORS.AllowedOrigins[1] == "https://other.com" + }, + desc: "should split and trim comma-separated origins", + }, + { + name: "methods override", + envKey: "CORS_ALLOWED_METHODS", + envVal: "GET,POST", + check: func(c *Config) bool { + return len(c.CORS.AllowedMethods) == 2 && + c.CORS.AllowedMethods[0] == "GET" && + c.CORS.AllowedMethods[1] == "POST" + }, + desc: "should split comma-separated methods", + }, + { + name: "headers override", + envKey: "CORS_ALLOWED_HEADERS", + envVal: "Authorization", + check: func(c *Config) bool { + return len(c.CORS.AllowedHeaders) == 1 && + c.CORS.AllowedHeaders[0] == "Authorization" + }, + desc: "should accept single header value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set the env var under test; clear all others to avoid cross-talk + os.Setenv(tt.envKey, tt.envVal) + defer os.Unsetenv(tt.envKey) + + // Also set TRUST_PROXY so validateSecurity doesn't reject default config + os.Setenv("TRUST_PROXY", "true") + defer os.Unsetenv("TRUST_PROXY") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if !tt.check(cfg) { + t.Fatalf("%s: check failed; origins=%v methods=%v headers=%v", + tt.desc, cfg.CORS.AllowedOrigins, cfg.CORS.AllowedMethods, cfg.CORS.AllowedHeaders) + } + }) + } +} + +func TestBoolEnvOverrides(t *testing.T) { + tests := []struct { + name string + envKey string + envVal string + check func(*Config) bool + }{ + { + name: "AUTH_ENABLED true", + envKey: "AUTH_ENABLED", + envVal: "true", + check: func(c *Config) bool { return c.Auth.Enabled }, + }, + { + name: "AUTH_ENABLED yes", + envKey: "AUTH_ENABLED", + envVal: "yes", + check: func(c *Config) bool { return c.Auth.Enabled }, + }, + { + name: "AUTH_ENABLED 1", + envKey: "AUTH_ENABLED", + envVal: "1", + check: func(c *Config) bool { return c.Auth.Enabled }, + }, + { + name: "TRUST_PROXY true", + envKey: "TRUST_PROXY", + envVal: "true", + check: func(c *Config) bool { return c.Auth.TrustProxy }, + }, + { + name: "STORAGE_METADATA_ONLY true", + envKey: "STORAGE_METADATA_ONLY", + envVal: "true", + check: func(c *Config) bool { return c.Storage.MetadataOnly }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + os.Setenv(tt.envKey, tt.envVal) + defer os.Unsetenv(tt.envKey) + + // Need TRUST_PROXY or AUTH_TOKEN for security validation when AUTH_ENABLED + if tt.envKey == "AUTH_ENABLED" { + os.Setenv("AUTH_TOKEN", "test-token") + defer os.Unsetenv("AUTH_TOKEN") + } + if tt.envKey != "TRUST_PROXY" { + os.Setenv("TRUST_PROXY", "true") + defer os.Unsetenv("TRUST_PROXY") + } + + cfg, err := Load() + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if !tt.check(cfg) { + t.Fatalf("expected %s=%s to set config field to true", tt.envKey, tt.envVal) + } + }) + } +} + +func TestSplitAndTrim(t *testing.T) { + tests := []struct { + input string + want []string + }{ + {"a,b,c", []string{"a", "b", "c"}}, + {" a , b , c ", []string{"a", "b", "c"}}, + {"single", []string{"single"}}, + {"a,,b", []string{"a", "b"}}, + {"", []string{}}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := splitAndTrim(tt.input) + if len(got) != len(tt.want) { + t.Fatalf("splitAndTrim(%q) = %v, want %v", tt.input, got, tt.want) + } + for i := range got { + if got[i] != tt.want[i] { + t.Fatalf("splitAndTrim(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i]) + } + } + }) + } +} + +func TestNoAnthropicConfigField(t *testing.T) { + // Verify the top-level Anthropic field was removed from Config. + // The canonical location is cfg.Providers.Anthropic. + cfg := defaultConfig() + if cfg.Providers.Anthropic.BaseURL == "" { + t.Fatal("expected Providers.Anthropic.BaseURL to have a default value") + } +} diff --git a/proxy/internal/handler/handlers.go b/proxy/internal/handler/handlers.go index 3f3c6f6..f696adf 100644 --- a/proxy/internal/handler/handlers.go +++ b/proxy/internal/handler/handlers.go @@ -2,7 +2,9 @@ package handler import ( "bytes" + "context" "crypto/rand" + "crypto/sha256" "encoding/hex" "encoding/json" "fmt" @@ -13,32 +15,54 @@ import ( "sort" "strconv" "strings" + "sync" "time" "github.com/gorilla/mux" "github.com/seifghazi/claude-code-monitor/internal/model" + "github.com/seifghazi/claude-code-monitor/internal/provider" + "github.com/seifghazi/claude-code-monitor/internal/runtime" "github.com/seifghazi/claude-code-monitor/internal/service" "github.com/seifghazi/claude-code-monitor/internal/sse" ) +const ( + defaultPage = 1 + defaultPageLimit = 10 + maxPageLimit = 100000 + defaultBucketMinutes = 60 + convHashCharLimit = 500 + convHashThreshold = 0.3 + maxUserAgentLen = 20 + maxStreamChunks = 2000 // max SSE chunks to hold in memory for logging +) + type Handler struct { - anthropicService service.AnthropicService storageService service.StorageService conversationService service.ConversationService modelRouter *service.ModelRouter + anthropicProvider provider.Provider logger *log.Logger + cachedSettings *model.ProxySettings + cachedSettingsMu sync.RWMutex + // demoteNonstreaming forces stream=true upstream when the client requested + // stream=false. The proxy then accumulates the SSE stream into a single + // non-streaming JSON response. Eliminates ResponseHeaderTimeout failures + // for long-running requests (1M context + extended thinking on opus). + demoteNonstreaming bool } -func New(anthropicService service.AnthropicService, storageService service.StorageService, logger *log.Logger, modelRouter *service.ModelRouter) *Handler { +func New(storageService service.StorageService, logger *log.Logger, modelRouter *service.ModelRouter, anthropicProvider provider.Provider, demoteNonstreaming bool) *Handler { conversationService := service.NewConversationService() return &Handler{ - anthropicService: anthropicService, storageService: storageService, conversationService: conversationService, modelRouter: modelRouter, + anthropicProvider: anthropicProvider, logger: logger, + demoteNonstreaming: demoteNonstreaming, } } @@ -52,7 +76,7 @@ func (h *Handler) Messages(w http.ResponseWriter, r *http.Request) { // Get body bytes from context (set by middleware) bodyBytes := getBodyBytes(r) if bodyBytes == nil { - http.Error(w, "Error reading request body", http.StatusBadRequest) + writeErrorResponse(w, "Error reading request body", http.StatusBadRequest) return } @@ -77,55 +101,101 @@ func (h *Handler) Messages(w http.ResponseWriter, r *http.Request) { // Create request log with routing information requestLog := &model.RequestLog{ - RequestID: requestID, - Timestamp: time.Now().Format(time.RFC3339), - Method: r.Method, - Endpoint: r.URL.Path, - Headers: SanitizeHeaders(r.Header), - Body: req, - Model: decision.OriginalModel, - OriginalModel: decision.OriginalModel, - RoutedModel: decision.TargetModel, - UserAgent: r.Header.Get("User-Agent"), - ContentType: r.Header.Get("Content-Type"), + RequestID: requestID, + Timestamp: time.Now().Format(time.RFC3339), + Method: r.Method, + Endpoint: r.URL.Path, + Headers: SanitizeHeaders(r.Header), + Body: req, + Model: decision.OriginalModel, + OriginalModel: decision.OriginalModel, + RoutedModel: decision.TargetModel, + UserAgent: r.Header.Get("User-Agent"), + ContentType: r.Header.Get("Content-Type"), + ConversationHash: computeConversationHash(&req), + MessageCount: len(req.Messages), } if _, err := h.storageService.SaveRequest(requestLog); err != nil { log.Printf("❌ Error saving request: %v", err) } - // If the model was changed by routing, update the request body - if decision.TargetModel != decision.OriginalModel { - req.Model = decision.TargetModel + // Decide whether to demote a non-streaming client request to a streaming + // upstream call. Anthropic's non-streaming responses don't write headers + // until the full body is computed, which causes ResponseHeaderTimeout + // failures on opus + 1M context + extended thinking. With demotion the + // proxy asks Anthropic for SSE, gets headers immediately, and synthesizes + // a single JSON response for the client. + clientWantsStream := req.Stream + demote := h.demoteNonstreaming && + !clientWantsStream && + decision.Provider.Name() == "anthropic" - // Re-marshal the request with the updated model - updatedBodyBytes, err := json.Marshal(req) + // Rewrite the upstream body if either the routed model differs from the + // original or we're demoting to streaming. Use a raw map (with json.Number + // to preserve integer precision in unknown nested fields like tool inputs + // from previous turns) so we don't drop fields the AnthropicRequest struct + // doesn't model (thinking, top_p, beta-only fields). The previous + // re-marshal-from-struct path silently dropped any field not declared on + // AnthropicRequest. + if demote || decision.TargetModel != decision.OriginalModel { + var raw map[string]interface{} + dec := json.NewDecoder(bytes.NewReader(bodyBytes)) + dec.UseNumber() + if err := dec.Decode(&raw); err != nil { + log.Printf("❌ Error parsing body for rewrite: %v", err) + writeErrorResponse(w, "Invalid JSON", http.StatusBadRequest) + return + } + if decision.TargetModel != decision.OriginalModel { + raw["model"] = decision.TargetModel + req.Model = decision.TargetModel + } + if demote { + raw["stream"] = true + } + updatedBodyBytes, err := json.Marshal(raw) if err != nil { log.Printf("❌ Error marshaling updated request: %v", err) writeErrorResponse(w, "Failed to process request", http.StatusInternalServerError) return } - - // Update the request body r.Body = io.NopCloser(bytes.NewReader(updatedBodyBytes)) r.ContentLength = int64(len(updatedBodyBytes)) r.Header.Set("Content-Length", fmt.Sprintf("%d", len(updatedBodyBytes))) } + // Create a context with an extended timeout for the forwarded request. + // We use a background context with a long timeout (30 minutes) instead of the + // request context to prevent "context canceled" errors for long-running API calls. + // This is necessary because: + // Use context.Background() instead of r.Context() so that: + // 1. Client disconnects don't cancel in-flight API calls (we still want to record usage) + // 2. The HTTP server's WriteTimeout doesn't cap the forwarding timeout + // 3. Claude's "thinking" feature can cause responses to take 5+ minutes + forwardCtx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + + // Apply request header rules before forwarding + h.applyRequestHeaderRules(r) + // Forward the request to the selected provider - resp, err := decision.Provider.ForwardRequest(r.Context(), r) + resp, err := decision.Provider.ForwardRequest(forwardCtx, r) if err != nil { - log.Printf("❌ Error forwarding to %s API: %v", decision.Provider.Name(), err) + logForwardFailure(r, &req, bodyBytes, decision, requestID, startTime, forwardCtx, err) writeErrorResponse(w, "Failed to forward request", http.StatusInternalServerError) return } defer resp.Body.Close() - if req.Stream { + if clientWantsStream { h.handleStreamingResponse(w, resp, requestLog, startTime) return } - + if demote { + h.handleDemotedStreamingResponse(w, resp, requestLog, startTime) + return + } h.handleNonStreamingResponse(w, resp, requestLog, startTime) } @@ -141,7 +211,124 @@ func (h *Handler) Models(w http.ResponseWriter, r *http.Request) { writeJSONResponse(w, response) } +// ProxyPassthrough forwards any unhandled /v1/* request to Anthropic and logs it. +// This covers endpoints like config, quota, batches, etc. +func (h *Handler) ProxyPassthrough(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + requestID := generateRequestID() + + // Read body if present + var bodyBytes []byte + if r.Body != nil && (r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH") { + var err error + bodyBytes, err = io.ReadAll(r.Body) + if err != nil { + log.Printf("Error reading passthrough request body: %v", err) + writeErrorResponse(w, "Error reading request body", http.StatusBadRequest) + return + } + r.Body.Close() + r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + } + + // Build request log + var bodyForLog interface{} + if len(bodyBytes) > 0 { + var parsed interface{} + if err := json.Unmarshal(bodyBytes, &parsed); err == nil { + bodyForLog = parsed + } else { + bodyForLog = string(bodyBytes) + } + } + + requestLog := &model.RequestLog{ + RequestID: requestID, + Timestamp: time.Now().Format(time.RFC3339), + Method: r.Method, + Endpoint: r.URL.Path, + Headers: SanitizeHeaders(r.Header), + Body: bodyForLog, + UserAgent: r.Header.Get("User-Agent"), + ContentType: r.Header.Get("Content-Type"), + } + + if _, err := h.storageService.SaveRequest(requestLog); err != nil { + log.Printf("Error saving passthrough request: %v", err) + } + + // Apply request header rules + h.applyRequestHeaderRules(r) + + // Forward to Anthropic + forwardCtx, cancel := context.WithTimeout(r.Context(), 2*time.Minute) + defer cancel() + + resp, err := h.anthropicProvider.ForwardRequest(forwardCtx, r) + if err != nil { + log.Printf("Error forwarding passthrough request to %s: %v", r.URL.Path, err) + writeErrorResponse(w, "Failed to forward request", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + // Read the response + responseBytes, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("Error reading passthrough response: %v", err) + writeErrorResponse(w, "Failed to read response", http.StatusInternalServerError) + return + } + + responseLog := &model.ResponseLog{ + StatusCode: resp.StatusCode, + Headers: SanitizeResponseHeaders(resp.Header), + ResponseTime: time.Since(startTime).Milliseconds(), + IsStreaming: false, + CompletedAt: time.Now().Format(time.RFC3339), + RateLimit: ExtractRateLimitInfo(resp.Header), + } + + // Store response body as JSON if possible, otherwise as text + var parsed interface{} + if err := json.Unmarshal(responseBytes, &parsed); err == nil { + responseLog.Body = json.RawMessage(responseBytes) + } else { + responseLog.BodyText = string(responseBytes) + } + + requestLog.Response = responseLog + extractOrganizationID(requestLog, resp.Header) + if err := h.storageService.UpdateRequestWithResponse(requestLog); err != nil { + log.Printf("Error updating passthrough request with response: %v", err) + } + + // Forward all response headers and body to the client + CopyAllResponseHeaders(w, resp) + h.applyResponseHeaderRules(w) + w.Header().Set("Content-Type", resp.Header.Get("Content-Type")) + w.WriteHeader(resp.StatusCode) + w.Write(responseBytes) + + log.Printf("Passthrough %s %s -> %d (%dms)", r.Method, r.URL.Path, resp.StatusCode, time.Since(startTime).Milliseconds()) +} + func (h *Handler) Health(w http.ResponseWriter, r *http.Request) { + // When the process is draining (SIGTERM received) we return 503 so any + // LB doing health-based routing — Traefik with a healthcheck on this + // service, or `deploy.sh wait_healthy` — stops sending new requests + // before the shutdown loop waits for in-flight to reach zero. + if runtime.IsDraining() { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "draining", + "timestamp": time.Now().Format(time.RFC3339), + "in_flight": runtime.InFlight(), + }) + return + } + response := &model.HealthResponse{ Status: "healthy", Timestamp: time.Now(), @@ -150,11 +337,25 @@ func (h *Handler) Health(w http.ResponseWriter, r *http.Request) { writeJSONResponse(w, response) } +// Livez exposes operational state for deploy/drain orchestration. Always +// returns 200 with the current in-flight count and draining flag — distinct +// from Health, which is a binary up/ready signal for load balancers. +func (h *Handler) Livez(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "ok", + "timestamp": time.Now().Format(time.RFC3339), + "in_flight": runtime.InFlight(), + "draining": runtime.IsDraining(), + }) +} + func (h *Handler) UI(w http.ResponseWriter, r *http.Request) { htmlContent, err := os.ReadFile("index.html") if err != nil { // Error reading index.html - http.Error(w, "UI not available", http.StatusNotFound) + writeErrorResponse(w, "UI not available", http.StatusNotFound) return } @@ -165,12 +366,12 @@ func (h *Handler) UI(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetRequests(w http.ResponseWriter, r *http.Request) { page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { - page = 1 + page = defaultPage } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit <= 0 { - limit = 10 // Default limit + limit = defaultPageLimit } // Get model filter from query parameters @@ -182,12 +383,11 @@ func (h *Handler) GetRequests(w http.ResponseWriter, r *http.Request) { requests, total, err := h.storageService.GetRequests(page, limit, modelFilter) if err != nil { log.Printf("Error getting requests: %v", err) - http.Error(w, "Failed to get requests", http.StatusInternalServerError) + writeErrorResponse(w, "Failed to get requests", http.StatusInternalServerError) return } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(struct { + writeJSONResponse(w, struct { Requests []model.RequestLog `json:"requests"` Total int `json:"total"` }{ @@ -217,9 +417,96 @@ func (h *Handler) NotFound(w http.ResponseWriter, r *http.Request) { writeErrorResponse(w, "Not found", http.StatusNotFound) } +// streamState holds the accumulated state while processing a streaming response. +type streamState struct { + fullResponseText strings.Builder + toolCalls []model.ContentBlock + streamingChunks []string + chunkTimings []model.ChunkTiming + finalUsage *model.AnthropicUsage + messageID string + modelName string + stopReason string + sawMessageStop bool + chunkIndex int + droppedChunks int // chunks not stored because we hit maxStreamChunks +} + +// extractMessageMetadata extracts message ID, model, and stop_reason from a message_start event. +func extractMessageMetadata(event map[string]interface{}) (id, modelName, stopReason string) { + if message, ok := event["message"].(map[string]interface{}); ok { + if v, ok := message["id"].(string); ok { + id = v + } + if v, ok := message["model"].(string); ok { + modelName = v + } + if v, ok := message["stop_reason"].(string); ok { + stopReason = v + } + } + return +} + +// extractUsageFromEvent extracts Anthropic usage data from a message_delta event. +// Returns nil if no usage data is present. +func extractUsageFromEvent(event map[string]interface{}) *model.AnthropicUsage { + usage, ok := event["usage"].(map[string]interface{}) + if !ok { + return nil + } + + u := &model.AnthropicUsage{} + if v, ok := usage["input_tokens"].(float64); ok { + u.InputTokens = int(v) + } + if v, ok := usage["output_tokens"].(float64); ok { + u.OutputTokens = int(v) + } + if v, ok := usage["cache_creation_input_tokens"].(float64); ok { + u.CacheCreationInputTokens = int(v) + } + if v, ok := usage["cache_read_input_tokens"].(float64); ok { + u.CacheReadInputTokens = int(v) + } + return u +} + +// buildStreamResponseBody assembles the Anthropic-format response body from accumulated stream state. +func buildStreamResponseBody(state *streamState) json.RawMessage { + var contentBlocks []model.AnthropicContentBlock + if state.fullResponseText.Len() > 0 { + contentBlocks = append(contentBlocks, model.AnthropicContentBlock{ + Type: "text", + Text: state.fullResponseText.String(), + }) + } + + responseBody := map[string]interface{}{ + "content": contentBlocks, + "id": state.messageID, + "model": state.modelName, + "role": "assistant", + "stop_reason": state.stopReason, + "type": "message", + } + + if state.finalUsage != nil { + responseBody["usage"] = state.finalUsage + } + + responseBodyBytes, err := json.Marshal(responseBody) + if err != nil { + log.Printf("❌ Error marshaling streaming response body: %v", err) + responseBodyBytes = []byte("{}") + } + return json.RawMessage(responseBodyBytes) +} + func (h *Handler) handleStreamingResponse(w http.ResponseWriter, resp *http.Response, requestLog *model.RequestLog, startTime time.Time) { // Forward important upstream headers (rate limits, request IDs, etc.) - ForwardResponseHeaders(w, resp) + CopyAllResponseHeaders(w, resp) + h.applyResponseHeaderRules(w) w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") @@ -232,14 +519,16 @@ func (h *Handler) handleStreamingResponse(w http.ResponseWriter, resp *http.Resp responseLog := &model.ResponseLog{ StatusCode: resp.StatusCode, - Headers: SanitizeHeaders(resp.Header), + Headers: SanitizeResponseHeaders(resp.Header), BodyText: string(errorBytes), ResponseTime: time.Since(startTime).Milliseconds(), IsStreaming: true, CompletedAt: time.Now().Format(time.RFC3339), + RateLimit: ExtractRateLimitInfo(resp.Header), } requestLog.Response = responseLog + extractOrganizationID(requestLog, resp.Header) if err := h.storageService.UpdateRequestWithResponse(requestLog); err != nil { log.Printf("❌ Error updating request with error response: %v", err) } @@ -249,83 +538,75 @@ func (h *Handler) handleStreamingResponse(w http.ResponseWriter, resp *http.Resp return } - var fullResponseText strings.Builder - var toolCalls []model.ContentBlock - var streamingChunks []string - var finalUsage *model.AnthropicUsage - var messageID string - var modelName string - var stopReason string - var sawMessageStop bool + state := &streamState{} streamErr := sse.ForEachLine(resp.Body, func(line string) error { - if line == "" || !strings.HasPrefix(line, "data:") { - return nil - } - - streamingChunks = append(streamingChunks, line) - if _, err := fmt.Fprintf(w, "%s\n\n", line); err != nil { + // Forward every SSE line verbatim — preserves event:, id:, retry:, + // `:` comment keepalives, and the blank-line event terminator. The + // previous code dropped everything except `data:` lines, which + // produced malformed SSE for clients that read the event field + // (browser EventSource, strict SSE parsers). + if _, err := fmt.Fprintf(w, "%s\n", line); err != nil { return err } if f, ok := w.(http.Flusher); ok { f.Flush() } + // Only `data:` lines carry the JSON payload we accumulate for storage + // and response synthesis. Everything else (event:, id:, comments, + // blank line) was just forwarded above and needs no further work. + if !strings.HasPrefix(line, "data:") { + return nil + } + + now := time.Now() + if state.chunkIndex < maxStreamChunks { + state.streamingChunks = append(state.streamingChunks, line) + state.chunkTimings = append(state.chunkTimings, model.ChunkTiming{ + Index: state.chunkIndex, + Timestamp: now.Format(time.RFC3339Nano), + ByteSize: len(line), + ElapsedMs: now.Sub(startTime).Milliseconds(), + }) + } else { + state.droppedChunks++ + } + state.chunkIndex++ + jsonData := strings.TrimPrefix(line, "data: ") - // Parse as generic JSON first to capture usage data + // Parse as generic JSON first to capture usage and metadata var genericEvent map[string]interface{} if err := json.Unmarshal([]byte(jsonData), &genericEvent); err != nil { log.Printf("⚠️ Error unmarshalling streaming event: %v", err) return nil } - // Capture metadata from message_start event - if eventType, ok := genericEvent["type"].(string); ok && eventType == "message_start" { - if message, ok := genericEvent["message"].(map[string]interface{}); ok { - // Capture message metadata - if id, ok := message["id"].(string); ok { - messageID = id - } - if model, ok := message["model"].(string); ok { - modelName = model - } - if reason, ok := message["stop_reason"].(string); ok { - stopReason = reason - } + eventType, _ := genericEvent["type"].(string) + + if eventType == "message_start" { + id, modelName, stopReason := extractMessageMetadata(genericEvent) + if id != "" { + state.messageID = id + } + if modelName != "" { + state.modelName = modelName + } + if stopReason != "" { + state.stopReason = stopReason } } - // Capture usage data from message_delta event - if eventType, ok := genericEvent["type"].(string); ok && eventType == "message_delta" { - // Usage is at top level for message_delta events - if usage, ok := genericEvent["usage"].(map[string]interface{}); ok { - // Create finalUsage if it doesn't exist yet - if finalUsage == nil { - finalUsage = &model.AnthropicUsage{} - } - - // Capture all usage fields - if inputTokens, ok := usage["input_tokens"].(float64); ok { - finalUsage.InputTokens = int(inputTokens) - } - if outputTokens, ok := usage["output_tokens"].(float64); ok { - finalUsage.OutputTokens = int(outputTokens) - } - if cacheCreation, ok := usage["cache_creation_input_tokens"].(float64); ok { - finalUsage.CacheCreationInputTokens = int(cacheCreation) - } - if cacheRead, ok := usage["cache_read_input_tokens"].(float64); ok { - finalUsage.CacheReadInputTokens = int(cacheRead) - } - + if eventType == "message_delta" { + if usage := extractUsageFromEvent(genericEvent); usage != nil { + state.finalUsage = usage } } // Parse as structured event for content processing var event model.StreamingEvent if err := json.Unmarshal([]byte(jsonData), &event); err != nil { - // Skip if structured parsing fails, but we already got the usage data above return nil } @@ -333,73 +614,45 @@ func (h *Handler) handleStreamingResponse(w http.ResponseWriter, resp *http.Resp case "content_block_delta": if event.Delta != nil { if event.Delta.Type == "text_delta" { - fullResponseText.WriteString(event.Delta.Text) + state.fullResponseText.WriteString(event.Delta.Text) } else if event.Delta.Type == "input_json_delta" { - if event.Index != nil && *event.Index < len(toolCalls) { - toolCalls[*event.Index].Input = append(toolCalls[*event.Index].Input, event.Delta.Input...) + if event.Index != nil && *event.Index < len(state.toolCalls) { + state.toolCalls[*event.Index].Input = append(state.toolCalls[*event.Index].Input, event.Delta.Input...) } } } case "content_block_start": if event.ContentBlock != nil && event.ContentBlock.Type == "tool_use" { - toolCalls = append(toolCalls, *event.ContentBlock) + state.toolCalls = append(state.toolCalls, *event.ContentBlock) } case "message_stop": - sawMessageStop = true + state.sawMessageStop = true } return nil }) - if streamErr == nil && !sawMessageStop { + if streamErr == nil && !state.sawMessageStop { streamErr = io.ErrUnexpectedEOF } responseLog := &model.ResponseLog{ StatusCode: resp.StatusCode, - Headers: SanitizeHeaders(resp.Header), - StreamingChunks: streamingChunks, + Headers: SanitizeResponseHeaders(resp.Header), + StreamingChunks: state.streamingChunks, + ChunkTimings: state.chunkTimings, ResponseTime: time.Since(startTime).Milliseconds(), IsStreaming: true, CompletedAt: time.Now().Format(time.RFC3339), + RateLimit: ExtractRateLimitInfo(resp.Header), } if streamErr != nil { responseLog.StreamError = streamErr.Error() } - // Create a structured response body that matches Anthropic's format - var contentBlocks []model.AnthropicContentBlock - if fullResponseText.Len() > 0 { - contentBlocks = append(contentBlocks, model.AnthropicContentBlock{ - Type: "text", - Text: fullResponseText.String(), - }) - } - - // Create an AnthropicResponse-like structure for consistency - responseBody := map[string]interface{}{ - "content": contentBlocks, - "id": messageID, - "model": modelName, - "role": "assistant", - "stop_reason": stopReason, - "type": "message", - } - - // Add usage data if we captured it - if finalUsage != nil { - responseBody["usage"] = finalUsage - } - - // Marshal to JSON for storage - responseBodyBytes, err := json.Marshal(responseBody) - if err != nil { - log.Printf("❌ Error marshaling streaming response body: %v", err) - responseBodyBytes = []byte("{}") - } - - responseLog.Body = json.RawMessage(responseBodyBytes) + responseLog.Body = buildStreamResponseBody(state) requestLog.Response = responseLog + extractOrganizationID(requestLog, resp.Header) if err := h.storageService.UpdateRequestWithResponse(requestLog); err != nil { log.Printf("❌ Error updating request with streaming response: %v", err) } @@ -427,7 +680,8 @@ func (h *Handler) handleStreamingResponse(w http.ResponseWriter, resp *http.Resp func (h *Handler) handleNonStreamingResponse(w http.ResponseWriter, resp *http.Response, requestLog *model.RequestLog, startTime time.Time) { // Forward important upstream headers (rate limits, request IDs, etc.) - ForwardResponseHeaders(w, resp) + CopyAllResponseHeaders(w, resp) + h.applyResponseHeaderRules(w) responseBytes, err := io.ReadAll(resp.Body) if err != nil { @@ -438,10 +692,11 @@ func (h *Handler) handleNonStreamingResponse(w http.ResponseWriter, resp *http.R responseLog := &model.ResponseLog{ StatusCode: resp.StatusCode, - Headers: SanitizeHeaders(resp.Header), + Headers: SanitizeResponseHeaders(resp.Header), ResponseTime: time.Since(startTime).Milliseconds(), IsStreaming: false, CompletedAt: time.Now().Format(time.RFC3339), + RateLimit: ExtractRateLimitInfo(resp.Header), } // Parse the response as AnthropicResponse for consistent structure @@ -453,7 +708,7 @@ func (h *Handler) handleNonStreamingResponse(w http.ResponseWriter, resp *http.R } else { // If parsing fails, store as text but log the error log.Printf("⚠️ Failed to parse Anthropic response: %v", err) - log.Printf("📄 Response body (first 500 chars): %s", string(responseBytes[:min(500, len(responseBytes))])) + log.Printf("📄 Response body (first %d chars): %s", convHashCharLimit, string(responseBytes[:min(convHashCharLimit, len(responseBytes))])) responseLog.BodyText = string(responseBytes) } } else { @@ -462,6 +717,7 @@ func (h *Handler) handleNonStreamingResponse(w http.ResponseWriter, resp *http.R } requestLog.Response = responseLog + extractOrganizationID(requestLog, resp.Header) if err := h.storageService.UpdateRequestWithResponse(requestLog); err != nil { log.Printf("❌ Error updating request with response: %v", err) } @@ -479,6 +735,20 @@ func (h *Handler) handleNonStreamingResponse(w http.ResponseWriter, resp *http.R w.Write(responseBytes) } +// applyRequestHeaderRules applies configured request header rules to the given request. +func (h *Handler) applyRequestHeaderRules(r *http.Request) { + if settings := h.GetCachedSettings(); len(settings.RequestHeaderRules) > 0 { + ApplyHeaderRules(r.Header, settings.RequestHeaderRules) + } +} + +// applyResponseHeaderRules applies configured response header rules to the response writer. +func (h *Handler) applyResponseHeaderRules(w http.ResponseWriter) { + if settings := h.GetCachedSettings(); len(settings.ResponseHeaderRules) > 0 { + ApplyHeaderRules(w.Header(), settings.ResponseHeaderRules) + } +} + // Helper function to get minimum of two integers func min(a, b int) int { if a < b { @@ -487,12 +757,126 @@ func min(a, b int) int { return b } +// extractOrganizationID extracts the Anthropic-Organization-Id from response headers and sets it on the request log. +func extractOrganizationID(requestLog *model.RequestLog, respHeaders http.Header) { + if orgID := respHeaders.Get("Anthropic-Organization-Id"); orgID != "" { + requestLog.OrganizationID = orgID + } +} + func generateRequestID() string { bytes := make([]byte, 8) - rand.Read(bytes) + if _, err := rand.Read(bytes); err != nil { + // fallback to timestamp-based ID + return fmt.Sprintf("%x", time.Now().UnixNano()) + } return hex.EncodeToString(bytes) } +// computeConversationHash generates a hash to identify which conversation a request belongs to. +// It collects plain text from the first user message that has real content (after stripping +// injected XML blocks), falling back to the system prompt hash if no user text is found. +func computeConversationHash(req *model.AnthropicRequest) string { + if req == nil || len(req.Messages) == 0 { + return "" + } + + // Try each user message for real text content + for _, msg := range req.Messages { + if msg.Role != "user" { + continue + } + + // Collect all text blocks from this message + var allText strings.Builder + blocks := msg.GetContentBlocks() + for _, block := range blocks { + if block.Type == "text" && block.Text != "" { + if allText.Len() > 0 { + allText.WriteString("\n") + } + allText.WriteString(block.Text) + } + } + + if allText.Len() == 0 { + continue + } + + // Strip all XML-like tags and their content (system-reminder, command-*, etc.) + text := stripXmlBlocks(allText.String()) + text = strings.TrimSpace(text) + + if text == "" { + continue + } + + // Take first N chars to avoid hashing huge messages + if len(text) > convHashCharLimit { + text = text[:convHashCharLimit] + } + + hash := sha256.Sum256([]byte(text)) + return hex.EncodeToString(hash[:8]) + } + + // Fallback: hash the system prompt if present (all turns share the same system prompt) + if len(req.System) > 0 { + var sysText strings.Builder + for _, s := range req.System { + if s.Text != "" { + sysText.WriteString(s.Text) + } + } + if sysText.Len() > 0 { + text := sysText.String() + if len(text) > convHashCharLimit { + text = text[:convHashCharLimit] + } + hash := sha256.Sum256([]byte("sys:" + text)) + return hex.EncodeToString(hash[:8]) + } + } + + return "" +} + +// stripXmlBlocks removes XML-like tag blocks from text, leaving only plain text. +func stripXmlBlocks(text string) string { + // Iteratively remove ... blocks + for { + start := strings.Index(text, "<") + if start == -1 { + break + } + // Find tag name + end := strings.IndexByte(text[start+1:], '>') + if end == -1 { + break + } + tagEnd := start + 1 + end + tagContent := text[start+1 : tagEnd] + // Skip self-closing or malformed + if strings.HasPrefix(tagContent, "/") || strings.HasSuffix(tagContent, "/") { + text = text[:start] + text[tagEnd+1:] + continue + } + tagName := strings.Fields(tagContent)[0] + // Look for closing tag + closeTag := "" + closeIdx := strings.Index(text[tagEnd+1:], closeTag) + if closeIdx == -1 { + // No closing tag — not a block, skip past this < + text = text[:start] + text[start+1:] + continue + } + // Remove the entire block + blockEnd := tagEnd + 1 + closeIdx + len(closeTag) + text = text[:start] + text[blockEnd:] + } + return text +} + func getBodyBytes(r *http.Request) []byte { if bodyBytes, ok := r.Context().Value(model.BodyBytesKey).([]byte); ok { return bodyBytes @@ -580,6 +964,10 @@ func extractTextFromMessage(message json.RawMessage) string { // Conversation handlers func (h *Handler) GetConversations(w http.ResponseWriter, r *http.Request) { + modelFilter := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("model"))) + if modelFilter == "" { + modelFilter = "all" + } conversations, err := h.conversationService.GetConversations() if err != nil { @@ -592,6 +980,10 @@ func (h *Handler) GetConversations(w http.ResponseWriter, r *http.Request) { var allConversations []map[string]interface{} for _, convs := range conversations { for _, conv := range convs { + if modelFilter != "all" && !conversationModelMatchesFilter(conv.Model, modelFilter) { + continue + } + // Extract first user message from the conversation var firstMessage string for _, msg := range conv.Messages { @@ -615,7 +1007,9 @@ func (h *Handler) GetConversations(w http.ResponseWriter, r *http.Request) { "lastActivity": conv.EndTime.Format(time.RFC3339), "duration": conv.EndTime.Sub(conv.StartTime).Milliseconds(), "firstMessage": firstMessage, + "projectPath": conv.ProjectPath, "projectName": conv.ProjectName, + "model": conv.Model, }) } } @@ -630,49 +1024,70 @@ func (h *Handler) GetConversations(w http.ResponseWriter, r *http.Request) { // Apply pagination page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { - page = 1 + page = defaultPage } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit <= 0 { - limit = 10 + limit = defaultPageLimit } + total := len(allConversations) start := (page - 1) * limit end := start + limit - if start > len(allConversations) { + hasMore := false + if start >= total { allConversations = []map[string]interface{}{} } else { - if end > len(allConversations) { - end = len(allConversations) + if end > total { + end = total } + hasMore = end < total allConversations = allConversations[start:end] } response := map[string]interface{}{ "conversations": allConversations, + "hasMore": hasMore, + "total": total, + "page": page, + "limit": limit, } writeJSONResponse(w, response) } +func conversationModelMatchesFilter(modelValue, filter string) bool { + if filter == "" || filter == "all" { + return true + } + if modelValue == "" { + return false + } + + modelValue = strings.ToLower(modelValue) + filter = strings.ToLower(filter) + + return strings.Contains(modelValue, filter) +} + func (h *Handler) GetConversationByID(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) sessionID, ok := vars["id"] if !ok { - http.Error(w, "Session ID is required", http.StatusBadRequest) + writeErrorResponse(w, "Session ID is required", http.StatusBadRequest) return } projectPath := r.URL.Query().Get("project") if projectPath == "" { - http.Error(w, "Project path is required", http.StatusBadRequest) + writeErrorResponse(w, "Project path is required", http.StatusBadRequest) return } conversation, err := h.conversationService.GetConversation(projectPath, sessionID) if err != nil { log.Printf("❌ Error getting conversation: %v", err) - http.Error(w, "Conversation not found", http.StatusNotFound) + writeErrorResponse(w, "Conversation not found", http.StatusNotFound) return } @@ -682,7 +1097,7 @@ func (h *Handler) GetConversationByID(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetConversationsByProject(w http.ResponseWriter, r *http.Request) { projectPath := r.URL.Query().Get("project") if projectPath == "" { - http.Error(w, "Project path is required", http.StatusBadRequest) + writeErrorResponse(w, "Project path is required", http.StatusBadRequest) return } @@ -695,3 +1110,613 @@ func (h *Handler) GetConversationsByProject(w http.ResponseWriter, r *http.Reque writeJSONResponse(w, conversations) } + +// GetStats returns aggregated usage statistics +func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) { + startDate := r.URL.Query().Get("start_date") + endDate := r.URL.Query().Get("end_date") + modelFilter := r.URL.Query().Get("model") + orgFilter := r.URL.Query().Get("org") + + stats, err := h.storageService.GetUsageStats(startDate, endDate, modelFilter, orgFilter) + if err != nil { + log.Printf("❌ Error getting usage stats: %v", err) + writeErrorResponse(w, "Failed to get usage statistics", http.StatusInternalServerError) + return + } + + writeJSONResponse(w, stats) +} + +// GetRequestsSummary returns lightweight request data for fast list rendering +func (h *Handler) GetRequestsSummary(w http.ResponseWriter, r *http.Request) { + modelFilter := r.URL.Query().Get("model") + if modelFilter == "" { + modelFilter = "all" + } + + // Get start/end time range (UTC ISO 8601 format from browser) + startTime := r.URL.Query().Get("start") + endTime := r.URL.Query().Get("end") + + // Parse pagination params + offset := 0 + limit := 0 // Default to 0 (no limit - fetch all) + + if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" { + if parsed, err := strconv.Atoi(offsetStr); err == nil && parsed >= 0 { + offset = parsed + } + } + + if limitStr := r.URL.Query().Get("limit"); limitStr != "" { + if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 && parsed <= maxPageLimit { + limit = parsed + } + } + + summaries, total, err := h.storageService.GetRequestsSummaryPaginated(modelFilter, startTime, endTime, offset, limit) + if err != nil { + log.Printf("Error getting request summaries: %v", err) + writeErrorResponse(w, "Failed to get requests", http.StatusInternalServerError) + return + } + + writeJSONResponse(w, struct { + Requests []*model.RequestSummary `json:"requests"` + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + }{ + Requests: summaries, + Total: total, + Offset: offset, + Limit: limit, + }) +} + +// GetRequestByID returns a single request by its ID +func (h *Handler) GetRequestByID(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + requestID := vars["id"] + + if requestID == "" { + writeErrorResponse(w, "Request ID is required", http.StatusBadRequest) + return + } + + request, fullID, err := h.storageService.GetRequestByShortID(requestID) + if err != nil { + log.Printf("Error getting request by ID %s: %v", requestID, err) + writeErrorResponse(w, "Failed to get request", http.StatusInternalServerError) + return + } + + if request == nil { + writeErrorResponse(w, "Request not found", http.StatusNotFound) + return + } + + writeJSONResponse(w, struct { + Request *model.RequestLog `json:"request"` + FullID string `json:"fullId"` + }{ + Request: request, + FullID: fullID, + }) +} + +// GetDashboardStats returns aggregated dashboard statistics (daily token usage) +func (h *Handler) GetDashboardStats(w http.ResponseWriter, r *http.Request) { + // Get start/end time range (UTC ISO 8601 format from browser) + startTime := r.URL.Query().Get("start") + endTime := r.URL.Query().Get("end") + + // Fallback to last 7 days if not provided + if startTime == "" || endTime == "" { + now := time.Now().UTC() + endTime = now.Format(time.RFC3339) + startTime = now.AddDate(0, 0, -7).Format(time.RFC3339) + } + + orgFilter := r.URL.Query().Get("org") + stats, err := h.storageService.GetStats(startTime, endTime, orgFilter) + if err != nil { + log.Printf("Error getting dashboard stats: %v", err) + writeErrorResponse(w, "Failed to get stats", http.StatusInternalServerError) + return + } + + writeJSONResponse(w, stats) +} + +// GetHourlyStats returns hourly breakdown for a specific date range +func (h *Handler) GetHourlyStats(w http.ResponseWriter, r *http.Request) { + // Get start/end time range (UTC ISO 8601 format from browser) + startTime := r.URL.Query().Get("start") + endTime := r.URL.Query().Get("end") + + if startTime == "" || endTime == "" { + writeErrorResponse(w, "start and end parameters are required", http.StatusBadRequest) + return + } + + bucketMinutes := defaultBucketMinutes + if b := r.URL.Query().Get("bucket"); b != "" { + if parsed, err := strconv.Atoi(b); err == nil && parsed > 0 { + bucketMinutes = parsed + } + } + + orgFilter := r.URL.Query().Get("org") + stats, err := h.storageService.GetHourlyStats(startTime, endTime, bucketMinutes, orgFilter) + if err != nil { + log.Printf("Error getting hourly stats: %v", err) + writeErrorResponse(w, "Failed to get hourly stats", http.StatusInternalServerError) + return + } + + writeJSONResponse(w, stats) +} + +// GetModelStats returns model breakdown for a specific date range +func (h *Handler) GetModelStats(w http.ResponseWriter, r *http.Request) { + // Get start/end time range (UTC ISO 8601 format from browser) + startTime := r.URL.Query().Get("start") + endTime := r.URL.Query().Get("end") + + if startTime == "" || endTime == "" { + writeErrorResponse(w, "start and end parameters are required", http.StatusBadRequest) + return + } + + orgFilter := r.URL.Query().Get("org") + stats, err := h.storageService.GetModelStats(startTime, endTime, orgFilter) + if err != nil { + log.Printf("Error getting model stats: %v", err) + writeErrorResponse(w, "Failed to get model stats", http.StatusInternalServerError) + return + } + + writeJSONResponse(w, stats) +} + +// GetOrganizations returns distinct organization IDs seen in requests +func (h *Handler) GetOrganizations(w http.ResponseWriter, r *http.Request) { + orgs, err := h.storageService.GetDistinctOrganizations() + if err != nil { + log.Printf("Error getting organizations: %v", err) + writeErrorResponse(w, "Failed to get organizations", http.StatusInternalServerError) + return + } + if orgs == nil { + orgs = []string{} + } + writeJSONResponse(w, struct { + Organizations []string `json:"organizations"` + }{Organizations: orgs}) +} + +// GetLatestRequestDate returns the date of the most recent request +func (h *Handler) GetLatestRequestDate(w http.ResponseWriter, r *http.Request) { + latestDate, err := h.storageService.GetLatestRequestDate() + if err != nil { + log.Printf("Error getting latest request date: %v", err) + writeErrorResponse(w, "Failed to get latest request date", http.StatusInternalServerError) + return + } + + writeJSONResponse(w, map[string]interface{}{ + "latestDate": latestDate, + }) +} + +// GetSettings returns the current proxy settings +func (h *Handler) GetSettings(w http.ResponseWriter, r *http.Request) { + settings, err := h.storageService.GetSettings() + if err != nil { + log.Printf("Error getting settings: %v", err) + writeErrorResponse(w, "Failed to get settings", http.StatusInternalServerError) + return + } + writeJSONResponse(w, settings) +} + +// SaveSettings updates the proxy settings +func (h *Handler) SaveSettings(w http.ResponseWriter, r *http.Request) { + var settings model.ProxySettings + if err := json.NewDecoder(r.Body).Decode(&settings); err != nil { + writeErrorResponse(w, "Invalid request body", http.StatusBadRequest) + return + } + if err := h.storageService.SaveSettings(&settings); err != nil { + log.Printf("Error saving settings: %v", err) + writeErrorResponse(w, "Failed to save settings", http.StatusInternalServerError) + return + } + // Update the in-memory cached settings + h.cachedSettingsMu.Lock() + h.cachedSettings = &settings + h.cachedSettingsMu.Unlock() + writeJSONResponse(w, settings) +} + +// GetHeaderRules returns the current header rules (convenience for the proxy to apply) +func (h *Handler) GetCachedSettings() *model.ProxySettings { + h.cachedSettingsMu.RLock() + if h.cachedSettings != nil { + settings := h.cachedSettings + h.cachedSettingsMu.RUnlock() + return settings + } + h.cachedSettingsMu.RUnlock() + + settings, err := h.storageService.GetSettings() + if err != nil { + log.Printf("Error loading settings: %v", err) + return &model.ProxySettings{} + } + + h.cachedSettingsMu.Lock() + defer h.cachedSettingsMu.Unlock() + if h.cachedSettings != nil { + return h.cachedSettings + } + h.cachedSettings = settings + return settings +} + +// logForwardFailure emits a structured key=value diagnostic line plus the +// existing human-readable error so timeouts to the upstream provider are +// debuggable from logs alone (request shape, body size, betas, thinking, etc.). +func logForwardFailure( + r *http.Request, + req *model.AnthropicRequest, + bodyBytes []byte, + decision *service.RoutingDecision, + requestID string, + startTime time.Time, + forwardCtx context.Context, + forwardErr error, +) { + hasThinking := false + var raw map[string]json.RawMessage + if json.Unmarshal(bodyBytes, &raw) == nil { + _, hasThinking = raw["thinking"] + } + + ctxErr := "" + if e := forwardCtx.Err(); e != nil { + ctxErr = e.Error() + } + provName := "" + if decision != nil && decision.Provider != nil { + provName = decision.Provider.Name() + } + origModel, routedModel := "", "" + if decision != nil { + origModel = decision.OriginalModel + routedModel = decision.TargetModel + } + + log.Printf( + "forward_error request_id=%s provider=%s model=%s routed_model=%s stream=%t body_bytes=%d messages=%d tools=%d max_tokens=%d has_thinking=%t query=%q anthropic_beta=%q client=%q elapsed=%s ctx_err=%s err=%q", + requestID, + provName, + origModel, + routedModel, + req.Stream, + len(bodyBytes), + len(req.Messages), + len(req.Tools), + req.MaxTokens, + hasThinking, + r.URL.RawQuery, + r.Header.Get("anthropic-beta"), + r.Header.Get("User-Agent"), + time.Since(startTime), + ctxErr, + forwardErr.Error(), + ) + + // Backwards-compat: keep the existing categorized line so anything grepping + // for "Error forwarding" or "Timeout forwarding" still matches. + switch forwardCtx.Err() { + case context.DeadlineExceeded: + log.Printf("❌ Timeout forwarding to %s API after 30 minutes: %v", provName, forwardErr) + case context.Canceled: + log.Printf("❌ Context canceled forwarding to %s API: %v", provName, forwardErr) + default: + log.Printf("❌ Error forwarding to %s API: %v", provName, forwardErr) + } +} + +// handleDemotedStreamingResponse consumes an Anthropic SSE stream and writes a +// single non-streaming JSON response to the client. Used when the client +// requested stream=false but we forced stream=true upstream to avoid the +// ResponseHeaderTimeout. Only invoked for the anthropic provider. +func (h *Handler) handleDemotedStreamingResponse(w http.ResponseWriter, resp *http.Response, requestLog *model.RequestLog, startTime time.Time) { + CopyAllResponseHeaders(w, resp) + h.applyResponseHeaderRules(w) + + // Upstream errors come back as JSON, not SSE — forward as-is with the + // correct content type for the non-streaming client. + if resp.StatusCode != http.StatusOK { + errorBytes, _ := io.ReadAll(resp.Body) + log.Printf("❌ Anthropic API error during demoted stream: %d %s", resp.StatusCode, string(errorBytes)) + responseLog := &model.ResponseLog{ + StatusCode: resp.StatusCode, + Headers: SanitizeResponseHeaders(resp.Header), + BodyText: string(errorBytes), + ResponseTime: time.Since(startTime).Milliseconds(), + IsStreaming: false, + CompletedAt: time.Now().Format(time.RFC3339), + RateLimit: ExtractRateLimitInfo(resp.Header), + } + requestLog.Response = responseLog + extractOrganizationID(requestLog, resp.Header) + if err := h.storageService.UpdateRequestWithResponse(requestLog); err != nil { + log.Printf("❌ Error updating request with error response: %v", err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.StatusCode) + w.Write(errorBytes) + return + } + + msg, accumErr := accumulateSSEToMessage(resp.Body) + + responseLog := &model.ResponseLog{ + StatusCode: resp.StatusCode, + Headers: SanitizeResponseHeaders(resp.Header), + ResponseTime: time.Since(startTime).Milliseconds(), + IsStreaming: false, // client-facing shape is non-streaming + CompletedAt: time.Now().Format(time.RFC3339), + RateLimit: ExtractRateLimitInfo(resp.Header), + } + if accumErr != nil { + responseLog.StreamError = accumErr.Error() + } + + var bodyBytes []byte + if accumErr == nil && msg != nil { + var marshalErr error + bodyBytes, marshalErr = json.Marshal(msg) + if marshalErr != nil { + log.Printf("❌ Error marshaling demoted response: %v", marshalErr) + accumErr = marshalErr + } else { + responseLog.Body = json.RawMessage(bodyBytes) + } + } + + requestLog.Response = responseLog + extractOrganizationID(requestLog, resp.Header) + if err := h.storageService.UpdateRequestWithResponse(requestLog); err != nil { + log.Printf("❌ Error updating request with response: %v", err) + } + + if accumErr != nil { + log.Printf("❌ Demotion accumulator error: %v", accumErr) + w.Header().Set("Content-Type", "application/json") + writeErrorResponse(w, fmt.Sprintf("Failed to assemble response: %v", accumErr), http.StatusBadGateway) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(bodyBytes) +} + +// accumulateSSEToMessage walks an Anthropic SSE stream and builds the +// equivalent non-streaming response body. Handles text, tool_use (with +// partial_json reassembly), thinking, and signature blocks indexed by +// content_block.index. Returns the message map ready for json.Marshal. +// +// Note: parses partial_json (the actual Anthropic field) for input_json_delta, +// not "input" — the existing model.Delta.Input wiring used by the legacy +// streaming accumulator never matched the wire format. +func accumulateSSEToMessage(body io.Reader) (map[string]interface{}, error) { + var ( + msgID, modelName, role, stopReason string + stopSequence interface{} + usage map[string]interface{} + sawMessageStop bool + ) + contentBlocks := map[int]map[string]interface{}{} + toolJSONBuilders := map[int]*strings.Builder{} + maxIndex := -1 + + err := sse.ForEachLine(body, func(line string) error { + if !strings.HasPrefix(line, "data:") { + return nil + } + var ev map[string]interface{} + if err := json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &ev); err != nil { + // Malformed event line — skip rather than abort the stream. + return nil + } + evType, _ := ev["type"].(string) + switch evType { + case "message_start": + msg, ok := ev["message"].(map[string]interface{}) + if !ok { + return nil + } + if v, ok := msg["id"].(string); ok { + msgID = v + } + if v, ok := msg["model"].(string); ok { + modelName = v + } + if v, ok := msg["role"].(string); ok { + role = v + } + if v, ok := msg["stop_reason"].(string); ok && v != "" { + stopReason = v + } + if v, ok := msg["stop_sequence"]; ok { + stopSequence = v + } + if u, ok := msg["usage"].(map[string]interface{}); ok { + usage = map[string]interface{}{} + for k, val := range u { + usage[k] = val + } + } + case "content_block_start": + idx, ok := indexFromEvent(ev) + if !ok { + return nil + } + cb, ok := ev["content_block"].(map[string]interface{}) + if !ok { + return nil + } + block := map[string]interface{}{} + for k, v := range cb { + block[k] = v + } + contentBlocks[idx] = block + if idx > maxIndex { + maxIndex = idx + } + if t, _ := block["type"].(string); t == "tool_use" { + toolJSONBuilders[idx] = &strings.Builder{} + // Tool input arrives via input_json_delta events; clear seed. + delete(block, "input") + } + case "content_block_delta": + idx, ok := indexFromEvent(ev) + if !ok { + return nil + } + block := contentBlocks[idx] + if block == nil { + return nil + } + delta, ok := ev["delta"].(map[string]interface{}) + if !ok { + return nil + } + dType, _ := delta["type"].(string) + switch dType { + case "text_delta": + cur, _ := block["text"].(string) + if t, ok := delta["text"].(string); ok { + block["text"] = cur + t + } + case "input_json_delta": + if pj, ok := delta["partial_json"].(string); ok { + if b, ok := toolJSONBuilders[idx]; ok { + b.WriteString(pj) + } + } + case "thinking_delta": + cur, _ := block["thinking"].(string) + if t, ok := delta["thinking"].(string); ok { + block["thinking"] = cur + t + } + case "signature_delta": + cur, _ := block["signature"].(string) + if s, ok := delta["signature"].(string); ok { + block["signature"] = cur + s + } + case "citations_delta": + // Anthropic streams each citation as its own delta event; + // non-streaming responses return them as a `citations` array + // on the content block. Append to preserve order. + if c, ok := delta["citation"]; ok { + existing, _ := block["citations"].([]interface{}) + block["citations"] = append(existing, c) + } + } + case "content_block_stop": + idx, ok := indexFromEvent(ev) + if !ok { + return nil + } + b, isToolBuilder := toolJSONBuilders[idx] + if !isToolBuilder { + return nil + } + block := contentBlocks[idx] + if block == nil { + return nil + } + s := b.String() + if s == "" { + block["input"] = map[string]interface{}{} + return nil + } + var input interface{} + if err := json.Unmarshal([]byte(s), &input); err != nil { + // Keep the raw partial JSON so a debugger can see what came through. + block["input_raw"] = s + return fmt.Errorf("tool_use input_json_delta did not parse as JSON at index %d: %w", idx, err) + } + block["input"] = input + case "message_delta": + if delta, ok := ev["delta"].(map[string]interface{}); ok { + if v, ok := delta["stop_reason"].(string); ok && v != "" { + stopReason = v + } + if v, ok := delta["stop_sequence"]; ok { + stopSequence = v + } + } + if u, ok := ev["usage"].(map[string]interface{}); ok { + if usage == nil { + usage = map[string]interface{}{} + } + for k, v := range u { + usage[k] = v + } + } + case "message_stop": + sawMessageStop = true + case "error": + if errObj, ok := ev["error"].(map[string]interface{}); ok { + m, _ := errObj["message"].(string) + t, _ := errObj["type"].(string) + return fmt.Errorf("upstream stream error: %s (%s)", m, t) + } + return fmt.Errorf("upstream stream error event without details") + } + return nil + }) + if err != nil { + return nil, err + } + if !sawMessageStop { + return nil, fmt.Errorf("stream ended before message_stop") + } + + blocks := make([]map[string]interface{}, 0, maxIndex+1) + for i := 0; i <= maxIndex; i++ { + if b := contentBlocks[i]; b != nil { + blocks = append(blocks, b) + } + } + + out := map[string]interface{}{ + "id": msgID, + "type": "message", + "role": role, + "model": modelName, + "content": blocks, + "stop_reason": stopReason, + "stop_sequence": stopSequence, + } + if usage != nil { + out["usage"] = usage + } + return out, nil +} + +func indexFromEvent(ev map[string]interface{}) (int, bool) { + f, ok := ev["index"].(float64) + if !ok { + return 0, false + } + return int(f), true +} diff --git a/proxy/internal/handler/handlers_conversations_test.go b/proxy/internal/handler/handlers_conversations_test.go new file mode 100644 index 0000000..a3ffb64 --- /dev/null +++ b/proxy/internal/handler/handlers_conversations_test.go @@ -0,0 +1,287 @@ +package handler + +import ( + "encoding/json" + "errors" + "io" + "log" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gorilla/mux" + + "github.com/seifghazi/claude-code-monitor/internal/service" +) + +type conversationServiceStub struct { + getConversationsFn func() (map[string][]*service.Conversation, error) + getConversationFn func(projectPath, sessionID string) (*service.Conversation, error) + getConversationsByProjectFn func(projectPath string) ([]*service.Conversation, error) +} + +func (s *conversationServiceStub) GetConversations() (map[string][]*service.Conversation, error) { + if s.getConversationsFn != nil { + return s.getConversationsFn() + } + panic("unexpected call") +} + +func (s *conversationServiceStub) GetConversation(projectPath, sessionID string) (*service.Conversation, error) { + if s.getConversationFn != nil { + return s.getConversationFn(projectPath, sessionID) + } + panic("unexpected call") +} + +func (s *conversationServiceStub) GetConversationsByProject(projectPath string) ([]*service.Conversation, error) { + if s.getConversationsByProjectFn != nil { + return s.getConversationsByProjectFn(projectPath) + } + panic("unexpected call") +} + +func newConversationHandler(stub *conversationServiceStub) *Handler { + return &Handler{ + conversationService: stub, + logger: log.New(io.Discard, "", 0), + } +} + +func rawMessage(t *testing.T, v interface{}) json.RawMessage { + t.Helper() + + data, err := json.Marshal(v) + if err != nil { + t.Fatalf("failed to marshal test raw message: %v", err) + } + return data +} + +func TestConversationModelMatchesFilter(t *testing.T) { + tests := []struct { + name string + model string + filter string + wantMatch bool + }{ + {name: "all matches any model", model: "claude-opus-4-6", filter: "all", wantMatch: true}, + {name: "opus matches opus tier", model: "claude-opus-4-6", filter: "opus", wantMatch: true}, + {name: "sonnet does not match opus tier", model: "claude-sonnet-4-5", filter: "opus", wantMatch: false}, + {name: "empty model does not match tier filter", model: "", filter: "haiku", wantMatch: false}, + {name: "substring filter still works", model: "claude-opus-4-6", filter: "opus-4", wantMatch: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := conversationModelMatchesFilter(tt.model, tt.filter); got != tt.wantMatch { + t.Fatalf("conversationModelMatchesFilter(%q, %q) = %v, want %v", tt.model, tt.filter, got, tt.wantMatch) + } + }) + } +} + +func TestGetConversationsAppliesFilterSortAndPagination(t *testing.T) { + oldest := time.Date(2026, 3, 18, 9, 0, 0, 0, time.UTC) + middle := time.Date(2026, 3, 19, 9, 0, 0, 0, time.UTC) + newest := time.Date(2026, 3, 20, 9, 0, 0, 0, time.UTC) + + stub := &conversationServiceStub{ + getConversationsFn: func() (map[string][]*service.Conversation, error) { + return map[string][]*service.Conversation{ + "proj-a": { + { + SessionID: "old-opus", + ProjectPath: "proj-a", + ProjectName: "Proj A", + Model: "claude-opus-4-6", + StartTime: oldest.Add(-5 * time.Minute), + EndTime: oldest, + MessageCount: 1, + Messages: []*service.ConversationMessage{ + {Type: "user", Message: rawMessage(t, "short prompt")}, + }, + }, + { + SessionID: "new-sonnet", + ProjectPath: "proj-a", + ProjectName: "Proj A", + Model: "claude-sonnet-4-5", + StartTime: newest.Add(-2 * time.Minute), + EndTime: newest, + MessageCount: 2, + Messages: []*service.ConversationMessage{ + {Type: "user", Message: rawMessage(t, []map[string]string{{"type": "text", "text": "newest prompt"}})}, + }, + }, + }, + "proj-b": { + { + SessionID: "mid-opus", + ProjectPath: "proj-b", + ProjectName: "Proj B", + Model: "claude-opus-4-6", + StartTime: middle.Add(-3 * time.Minute), + EndTime: middle, + MessageCount: 3, + Messages: []*service.ConversationMessage{ + {Type: "user", Message: rawMessage(t, map[string]string{"content": "middle prompt"})}, + }, + }, + }, + }, nil + }, + } + + req := httptest.NewRequest(http.MethodGet, "/api/conversations?model=opus&page=1&limit=1", nil) + rr := httptest.NewRecorder() + + newConversationHandler(stub).GetConversations(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response struct { + Conversations []map[string]interface{} `json:"conversations"` + HasMore bool `json:"hasMore"` + Total int `json:"total"` + Page int `json:"page"` + Limit int `json:"limit"` + } + decodeJSONBody(t, rr, &response) + + if response.Total != 2 || !response.HasMore || response.Page != 1 || response.Limit != 1 { + t.Fatalf("unexpected pagination metadata: %#v", response) + } + if len(response.Conversations) != 1 { + t.Fatalf("expected one paginated conversation, got %d", len(response.Conversations)) + } + + first := response.Conversations[0] + if first["id"] != "mid-opus" { + t.Fatalf("expected newest matching opus conversation first, got %#v", first) + } + if first["firstMessage"] != "middle prompt" { + t.Fatalf("expected extracted first message, got %#v", first["firstMessage"]) + } +} + +func TestGetConversationsReturnsInternalServerErrorOnServiceFailure(t *testing.T) { + stub := &conversationServiceStub{ + getConversationsFn: func() (map[string][]*service.Conversation, error) { + return nil, errors.New("boom") + }, + } + req := httptest.NewRequest(http.MethodGet, "/api/conversations", nil) + rr := httptest.NewRecorder() + + newConversationHandler(stub).GetConversations(rr, req) + + if rr.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", rr.Code) + } + + var response struct { + Error string `json:"error"` + } + decodeJSONBody(t, rr, &response) + if response.Error != "Failed to get conversations" { + t.Fatalf("unexpected error response: %#v", response) + } +} + +func TestGetConversationByIDRequiresProjectAndReturnsConversation(t *testing.T) { + t.Run("missing project", func(t *testing.T) { + req := mux.SetURLVars(httptest.NewRequest(http.MethodGet, "/api/conversations/session-1", nil), map[string]string{"id": "session-1"}) + rr := httptest.NewRecorder() + + newConversationHandler(&conversationServiceStub{}).GetConversationByID(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d", rr.Code) + } + + var response struct { + Error string `json:"error"` + } + decodeJSONBody(t, rr, &response) + if response.Error != "Project path is required" { + t.Fatalf("unexpected error response: %#v", response) + } + }) + + t.Run("success", func(t *testing.T) { + stub := &conversationServiceStub{ + getConversationFn: func(projectPath, sessionID string) (*service.Conversation, error) { + if projectPath != "team/app" || sessionID != "session-1" { + t.Fatalf("unexpected conversation lookup %q %q", projectPath, sessionID) + } + return &service.Conversation{ + SessionID: "session-1", + ProjectPath: "team/app", + ProjectName: "app", + Model: "claude-opus-4-6", + StartTime: time.Date(2026, 3, 20, 8, 0, 0, 0, time.UTC), + EndTime: time.Date(2026, 3, 20, 8, 5, 0, 0, time.UTC), + MessageCount: 2, + }, nil + }, + } + req := mux.SetURLVars(httptest.NewRequest(http.MethodGet, "/api/conversations/session-1?project=team/app", nil), map[string]string{"id": "session-1"}) + rr := httptest.NewRecorder() + + newConversationHandler(stub).GetConversationByID(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response service.Conversation + decodeJSONBody(t, rr, &response) + if response.SessionID != "session-1" || response.ProjectPath != "team/app" { + t.Fatalf("unexpected conversation payload: %#v", response) + } + }) +} + +func TestGetConversationsByProjectRequiresProjectAndHandlesFailure(t *testing.T) { + t.Run("missing project", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/conversations/project", nil) + rr := httptest.NewRecorder() + + newConversationHandler(&conversationServiceStub{}).GetConversationsByProject(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d", rr.Code) + } + }) + + t.Run("service failure", func(t *testing.T) { + stub := &conversationServiceStub{ + getConversationsByProjectFn: func(projectPath string) ([]*service.Conversation, error) { + if projectPath != "team/app" { + t.Fatalf("unexpected project path %q", projectPath) + } + return nil, errors.New("boom") + }, + } + req := httptest.NewRequest(http.MethodGet, "/api/conversations/project?project=team/app", nil) + rr := httptest.NewRecorder() + + newConversationHandler(stub).GetConversationsByProject(rr, req) + + if rr.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", rr.Code) + } + + var response struct { + Error string `json:"error"` + } + decodeJSONBody(t, rr, &response) + if response.Error != "Failed to get project conversations" { + t.Fatalf("unexpected error response: %#v", response) + } + }) +} diff --git a/proxy/internal/handler/handlers_dashboard_test.go b/proxy/internal/handler/handlers_dashboard_test.go new file mode 100644 index 0000000..35377f1 --- /dev/null +++ b/proxy/internal/handler/handlers_dashboard_test.go @@ -0,0 +1,630 @@ +package handler + +import ( + "encoding/json" + "errors" + "io" + "log" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gorilla/mux" + + "github.com/seifghazi/claude-code-monitor/internal/config" + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +type dashboardStorageStub struct { + getRequestsFn func(page, limit int, modelFilter string) ([]model.RequestLog, int, error) + getUsageStatsFn func(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) + getRequestsSummaryPaginatedFn func(modelFilter, startTime, endTime string, offset, limit int) ([]*model.RequestSummary, int, error) + getRequestByShortIDFn func(shortID string) (*model.RequestLog, string, error) + getStatsFn func(startDate, endDate, orgFilter string) (*model.DashboardStats, error) + getHourlyStatsFn func(startTime, endTime string, bucketMinutes int, orgFilter string) (*model.HourlyStatsResponse, error) + getModelStatsFn func(startTime, endTime, orgFilter string) (*model.ModelStatsResponse, error) + getDistinctOrganizationsFn func() ([]string, error) + getLatestRequestDateFn func() (*time.Time, error) + getSettingsFn func() (*model.ProxySettings, error) + saveSettingsFn func(settings *model.ProxySettings) error + clearRequestsFn func() (int, error) +} + +func (s *dashboardStorageStub) SaveRequest(*model.RequestLog) (string, error) { + panic("unexpected call") +} +func (s *dashboardStorageStub) GetRequests(page, limit int, modelFilter string) ([]model.RequestLog, int, error) { + if s.getRequestsFn != nil { + return s.getRequestsFn(page, limit, modelFilter) + } + panic("unexpected call") +} +func (s *dashboardStorageStub) GetAllRequests(string) ([]*model.RequestLog, error) { + panic("unexpected call") +} +func (s *dashboardStorageStub) GetRequestByShortID(shortID string) (*model.RequestLog, string, error) { + if s.getRequestByShortIDFn != nil { + return s.getRequestByShortIDFn(shortID) + } + panic("unexpected call") +} +func (s *dashboardStorageStub) ClearRequests() (int, error) { + if s.clearRequestsFn != nil { + return s.clearRequestsFn() + } + panic("unexpected call") +} +func (s *dashboardStorageStub) UpdateRequestWithGrading(string, *model.PromptGrade) error { + panic("unexpected call") +} +func (s *dashboardStorageStub) UpdateRequestWithResponse(*model.RequestLog) error { + panic("unexpected call") +} +func (s *dashboardStorageStub) DeleteRequestsOlderThan(time.Duration) (int, error) { + panic("unexpected call") +} +func (s *dashboardStorageStub) GetDatabaseStats() (map[string]interface{}, error) { + panic("unexpected call") +} +func (s *dashboardStorageStub) GetUsageStats(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) { + if s.getUsageStatsFn != nil { + return s.getUsageStatsFn(startDate, endDate, modelFilter, orgFilter) + } + panic("unexpected call") +} +func (s *dashboardStorageStub) GetRequestsSummary(string) ([]*model.RequestSummary, error) { + panic("unexpected call") +} +func (s *dashboardStorageStub) GetRequestsSummaryPaginated(modelFilter, startTime, endTime string, offset, limit int) ([]*model.RequestSummary, int, error) { + if s.getRequestsSummaryPaginatedFn != nil { + return s.getRequestsSummaryPaginatedFn(modelFilter, startTime, endTime, offset, limit) + } + panic("unexpected call") +} +func (s *dashboardStorageStub) GetStats(startDate, endDate, orgFilter string) (*model.DashboardStats, error) { + if s.getStatsFn != nil { + return s.getStatsFn(startDate, endDate, orgFilter) + } + panic("unexpected call") +} +func (s *dashboardStorageStub) GetHourlyStats(startTime, endTime string, bucketMinutes int, orgFilter string) (*model.HourlyStatsResponse, error) { + if s.getHourlyStatsFn != nil { + return s.getHourlyStatsFn(startTime, endTime, bucketMinutes, orgFilter) + } + panic("unexpected call") +} +func (s *dashboardStorageStub) GetModelStats(startTime, endTime, orgFilter string) (*model.ModelStatsResponse, error) { + if s.getModelStatsFn != nil { + return s.getModelStatsFn(startTime, endTime, orgFilter) + } + panic("unexpected call") +} +func (s *dashboardStorageStub) GetLatestRequestDate() (*time.Time, error) { + if s.getLatestRequestDateFn != nil { + return s.getLatestRequestDateFn() + } + panic("unexpected call") +} +func (s *dashboardStorageStub) GetDistinctOrganizations() ([]string, error) { + if s.getDistinctOrganizationsFn != nil { + return s.getDistinctOrganizationsFn() + } + panic("unexpected call") +} +func (s *dashboardStorageStub) GetSettings() (*model.ProxySettings, error) { + if s.getSettingsFn != nil { + return s.getSettingsFn() + } + return &model.ProxySettings{}, nil +} +func (s *dashboardStorageStub) SaveSettings(settings *model.ProxySettings) error { + if s.saveSettingsFn != nil { + return s.saveSettingsFn(settings) + } + return nil +} +func (s *dashboardStorageStub) GetConfig() *config.StorageConfig { return &config.StorageConfig{} } +func (s *dashboardStorageStub) EnsureDirectoryExists() error { return nil } +func (s *dashboardStorageStub) Close() error { return nil } + +func newTestHandler(storage *dashboardStorageStub) *Handler { + return &Handler{ + storageService: storage, + logger: log.New(io.Discard, "", 0), + } +} + +func decodeJSONBody(t *testing.T, rr *httptest.ResponseRecorder, dest interface{}) { + t.Helper() + + if err := json.NewDecoder(rr.Body).Decode(dest); err != nil { + t.Fatalf("failed decoding JSON response: %v", err) + } +} + +func TestGetRequestsUsesDefaultPaginationAndModelFilter(t *testing.T) { + storage := &dashboardStorageStub{ + getRequestsFn: func(page, limit int, modelFilter string) ([]model.RequestLog, int, error) { + if page != defaultPage { + t.Fatalf("expected page %d, got %d", defaultPage, page) + } + if limit != defaultPageLimit { + t.Fatalf("expected limit %d, got %d", defaultPageLimit, limit) + } + if modelFilter != "all" { + t.Fatalf("expected model filter all, got %q", modelFilter) + } + + return []model.RequestLog{{RequestID: "req-1"}}, 7, nil + }, + } + req := httptest.NewRequest(http.MethodGet, "/api/requests", nil) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetRequests(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response struct { + Requests []model.RequestLog `json:"requests"` + Total int `json:"total"` + } + decodeJSONBody(t, rr, &response) + + if len(response.Requests) != 1 || response.Requests[0].RequestID != "req-1" { + t.Fatalf("unexpected requests payload: %#v", response.Requests) + } + if response.Total != 7 { + t.Fatalf("expected total 7, got %d", response.Total) + } +} + +func TestGetRequestsReturnsInternalServerErrorOnStorageFailure(t *testing.T) { + storage := &dashboardStorageStub{ + getRequestsFn: func(page, limit int, modelFilter string) ([]model.RequestLog, int, error) { + return nil, 0, errors.New("boom") + }, + } + req := httptest.NewRequest(http.MethodGet, "/api/requests?page=2&limit=25&model=opus", nil) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetRequests(rr, req) + + if rr.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", rr.Code) + } + + var response model.ErrorResponse + decodeJSONBody(t, rr, &response) + if response.Error != "Failed to get requests" { + t.Fatalf("unexpected error response: %#v", response) + } +} + +func TestDeleteRequestsReturnsDeletedCountAndStorageErrors(t *testing.T) { + t.Run("success", func(t *testing.T) { + storage := &dashboardStorageStub{ + clearRequestsFn: func() (int, error) { + return 12, nil + }, + } + req := httptest.NewRequest(http.MethodDelete, "/api/requests", nil) + rr := httptest.NewRecorder() + + newTestHandler(storage).DeleteRequests(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response struct { + Message string `json:"message"` + Deleted int `json:"deleted"` + } + decodeJSONBody(t, rr, &response) + if response.Message != "Request history cleared" || response.Deleted != 12 { + t.Fatalf("unexpected delete response: %#v", response) + } + }) + + t.Run("storage error", func(t *testing.T) { + storage := &dashboardStorageStub{ + clearRequestsFn: func() (int, error) { + return 0, errors.New("boom") + }, + } + req := httptest.NewRequest(http.MethodDelete, "/api/requests", nil) + rr := httptest.NewRecorder() + + newTestHandler(storage).DeleteRequests(rr, req) + + if rr.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", rr.Code) + } + + var response model.ErrorResponse + decodeJSONBody(t, rr, &response) + if response.Error != "Error clearing request history" { + t.Fatalf("unexpected error response: %#v", response) + } + }) +} + +func TestGetRequestsSummaryNormalizesPaginationInputs(t *testing.T) { + storage := &dashboardStorageStub{ + getRequestsSummaryPaginatedFn: func(modelFilter, startTime, endTime string, offset, limit int) ([]*model.RequestSummary, int, error) { + if modelFilter != "all" { + t.Fatalf("expected default model filter all, got %q", modelFilter) + } + if startTime != "2026-03-01T00:00:00Z" { + t.Fatalf("unexpected start time %q", startTime) + } + if endTime != "2026-03-02T00:00:00Z" { + t.Fatalf("unexpected end time %q", endTime) + } + if offset != 0 { + t.Fatalf("expected invalid negative offset to normalize to 0, got %d", offset) + } + if limit != 0 { + t.Fatalf("expected invalid oversize limit to normalize to 0, got %d", limit) + } + + return []*model.RequestSummary{{RequestID: "req-summary-1"}}, 1, nil + }, + } + req := httptest.NewRequest(http.MethodGet, "/api/requests/summary?start=2026-03-01T00:00:00Z&end=2026-03-02T00:00:00Z&offset=-4&limit=100001", nil) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetRequestsSummary(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response struct { + Requests []*model.RequestSummary `json:"requests"` + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + } + decodeJSONBody(t, rr, &response) + + if len(response.Requests) != 1 || response.Requests[0].RequestID != "req-summary-1" { + t.Fatalf("unexpected summaries payload: %#v", response.Requests) + } + if response.Total != 1 || response.Offset != 0 || response.Limit != 0 { + t.Fatalf("unexpected summary metadata: %#v", response) + } +} + +func TestGetRequestByIDHandlesMissingAndNotFoundIDs(t *testing.T) { + t.Run("missing id", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/requests/", nil) + rr := httptest.NewRecorder() + + newTestHandler(&dashboardStorageStub{}).GetRequestByID(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d", rr.Code) + } + + var response model.ErrorResponse + decodeJSONBody(t, rr, &response) + if response.Error != "Request ID is required" { + t.Fatalf("unexpected error response: %#v", response) + } + }) + + t.Run("not found", func(t *testing.T) { + storage := &dashboardStorageStub{ + getRequestByShortIDFn: func(shortID string) (*model.RequestLog, string, error) { + if shortID != "abc123" { + t.Fatalf("expected short ID abc123, got %q", shortID) + } + return nil, "", nil + }, + } + req := mux.SetURLVars(httptest.NewRequest(http.MethodGet, "/api/requests/abc123", nil), map[string]string{"id": "abc123"}) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetRequestByID(rr, req) + + if rr.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", rr.Code) + } + + var response model.ErrorResponse + decodeJSONBody(t, rr, &response) + if response.Error != "Request not found" { + t.Fatalf("unexpected error response: %#v", response) + } + }) +} + +func TestGetRequestByIDReturnsRequestPayloadAndStorageErrors(t *testing.T) { + t.Run("success", func(t *testing.T) { + storage := &dashboardStorageStub{ + getRequestByShortIDFn: func(shortID string) (*model.RequestLog, string, error) { + if shortID != "abc123" { + t.Fatalf("expected short ID abc123, got %q", shortID) + } + return &model.RequestLog{RequestID: "full-request-id", Model: "claude-opus-4-6"}, "full-request-id", nil + }, + } + req := mux.SetURLVars(httptest.NewRequest(http.MethodGet, "/api/requests/abc123", nil), map[string]string{"id": "abc123"}) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetRequestByID(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response struct { + Request *model.RequestLog `json:"request"` + FullID string `json:"fullId"` + } + decodeJSONBody(t, rr, &response) + if response.Request == nil || response.Request.RequestID != "full-request-id" || response.FullID != "full-request-id" { + t.Fatalf("unexpected request payload: %#v", response) + } + }) + + t.Run("storage error", func(t *testing.T) { + storage := &dashboardStorageStub{ + getRequestByShortIDFn: func(shortID string) (*model.RequestLog, string, error) { + return nil, "", errors.New("boom") + }, + } + req := mux.SetURLVars(httptest.NewRequest(http.MethodGet, "/api/requests/abc123", nil), map[string]string{"id": "abc123"}) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetRequestByID(rr, req) + + if rr.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", rr.Code) + } + + var response model.ErrorResponse + decodeJSONBody(t, rr, &response) + if response.Error != "Failed to get request" { + t.Fatalf("unexpected error response: %#v", response) + } + }) +} + +func TestGetDashboardStatsFallsBackToLastSevenDays(t *testing.T) { + storage := &dashboardStorageStub{ + getStatsFn: func(startDate, endDate, orgFilter string) (*model.DashboardStats, error) { + if orgFilter != "org-1" { + t.Fatalf("expected org filter org-1, got %q", orgFilter) + } + + start, err := time.Parse(time.RFC3339, startDate) + if err != nil { + t.Fatalf("expected RFC3339 start date, got %q: %v", startDate, err) + } + end, err := time.Parse(time.RFC3339, endDate) + if err != nil { + t.Fatalf("expected RFC3339 end date, got %q: %v", endDate, err) + } + + diff := end.Sub(start) + if diff < (7*24*time.Hour-time.Second) || diff > (7*24*time.Hour+time.Second) { + t.Fatalf("expected ~7 day fallback window, got %v", diff) + } + + return &model.DashboardStats{ + DailyStats: []model.DailyTokens{{Date: "2026-03-20", Tokens: 42, Requests: 2}}, + }, nil + }, + } + req := httptest.NewRequest(http.MethodGet, "/api/dashboard/stats?org=org-1", nil) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetDashboardStats(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response model.DashboardStats + decodeJSONBody(t, rr, &response) + if len(response.DailyStats) != 1 || response.DailyStats[0].Tokens != 42 { + t.Fatalf("unexpected dashboard stats payload: %#v", response) + } +} + +func TestGetStatsPassesQueryFiltersThrough(t *testing.T) { + storage := &dashboardStorageStub{ + getUsageStatsFn: func(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) { + if startDate != "2026-03-01" { + t.Fatalf("expected start date 2026-03-01, got %q", startDate) + } + if endDate != "2026-03-07" { + t.Fatalf("expected end date 2026-03-07, got %q", endDate) + } + if modelFilter != "claude-sonnet-4-5" { + t.Fatalf("expected model filter claude-sonnet-4-5, got %q", modelFilter) + } + if orgFilter != "org-usage" { + t.Fatalf("expected org filter org-usage, got %q", orgFilter) + } + + return &model.UsageStats{ + TotalRequests: 3, + RequestsByModel: map[string]model.ModelStats{ + "claude-sonnet-4-5": {RequestCount: 3}, + }, + }, nil + }, + } + req := httptest.NewRequest(http.MethodGet, "/api/stats?start_date=2026-03-01&end_date=2026-03-07&model=claude-sonnet-4-5&org=org-usage", nil) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetStats(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response model.UsageStats + decodeJSONBody(t, rr, &response) + if response.TotalRequests != 3 { + t.Fatalf("expected total requests 3, got %d", response.TotalRequests) + } + if response.RequestsByModel["claude-sonnet-4-5"].RequestCount != 3 { + t.Fatalf("unexpected usage stats payload: %#v", response) + } +} + +func TestGetHourlyStatsValidatesRangeAndDefaultsBucket(t *testing.T) { + t.Run("missing range", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/dashboard/hourly?start=2026-03-01T00:00:00Z", nil) + rr := httptest.NewRecorder() + + newTestHandler(&dashboardStorageStub{}).GetHourlyStats(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d", rr.Code) + } + + var response model.ErrorResponse + decodeJSONBody(t, rr, &response) + if response.Error != "start and end parameters are required" { + t.Fatalf("unexpected error response: %#v", response) + } + }) + + t.Run("invalid bucket falls back to default", func(t *testing.T) { + storage := &dashboardStorageStub{ + getHourlyStatsFn: func(startTime, endTime string, bucketMinutes int, orgFilter string) (*model.HourlyStatsResponse, error) { + if startTime != "2026-03-01T00:00:00Z" || endTime != "2026-03-01T12:00:00Z" { + t.Fatalf("unexpected time range %q - %q", startTime, endTime) + } + if bucketMinutes != defaultBucketMinutes { + t.Fatalf("expected default bucket %d, got %d", defaultBucketMinutes, bucketMinutes) + } + if orgFilter != "org-2" { + t.Fatalf("expected org filter org-2, got %q", orgFilter) + } + + return &model.HourlyStatsResponse{ + HourlyStats: []model.HourlyTokens{{Hour: 9, Tokens: 123, Requests: 3}}, + }, nil + }, + } + req := httptest.NewRequest(http.MethodGet, "/api/dashboard/hourly?start=2026-03-01T00:00:00Z&end=2026-03-01T12:00:00Z&bucket=bad&org=org-2", nil) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetHourlyStats(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response model.HourlyStatsResponse + decodeJSONBody(t, rr, &response) + if len(response.HourlyStats) != 1 || response.HourlyStats[0].Tokens != 123 { + t.Fatalf("unexpected hourly stats payload: %#v", response) + } + }) +} + +func TestGetModelStatsRejectsMissingRange(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/dashboard/models?end=2026-03-01T12:00:00Z", nil) + rr := httptest.NewRecorder() + + newTestHandler(&dashboardStorageStub{}).GetModelStats(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d", rr.Code) + } + + var response model.ErrorResponse + decodeJSONBody(t, rr, &response) + if response.Error != "start and end parameters are required" { + t.Fatalf("unexpected error response: %#v", response) + } +} + +func TestGetModelStatsPassesFiltersThrough(t *testing.T) { + storage := &dashboardStorageStub{ + getModelStatsFn: func(startTime, endTime, orgFilter string) (*model.ModelStatsResponse, error) { + if startTime != "2026-03-01T00:00:00Z" || endTime != "2026-03-01T12:00:00Z" { + t.Fatalf("unexpected range %q - %q", startTime, endTime) + } + if orgFilter != "org-models" { + t.Fatalf("expected org filter org-models, got %q", orgFilter) + } + return &model.ModelStatsResponse{ + ModelStats: []model.ModelTokens{{Model: "claude-opus-4-6", Tokens: 321, Requests: 4}}, + }, nil + }, + } + req := httptest.NewRequest(http.MethodGet, "/api/dashboard/models?start=2026-03-01T00:00:00Z&end=2026-03-01T12:00:00Z&org=org-models", nil) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetModelStats(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response model.ModelStatsResponse + decodeJSONBody(t, rr, &response) + if len(response.ModelStats) != 1 || response.ModelStats[0].Model != "claude-opus-4-6" { + t.Fatalf("unexpected model stats payload: %#v", response) + } +} + +func TestGetOrganizationsReturnsEmptySliceWhenStorageReturnsNil(t *testing.T) { + storage := &dashboardStorageStub{ + getDistinctOrganizationsFn: func() ([]string, error) { + return nil, nil + }, + } + req := httptest.NewRequest(http.MethodGet, "/api/organizations", nil) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetOrganizations(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response struct { + Organizations []string `json:"organizations"` + } + decodeJSONBody(t, rr, &response) + if len(response.Organizations) != 0 { + t.Fatalf("expected empty organizations list, got %#v", response.Organizations) + } +} + +func TestGetLatestRequestDateReturnsNullWhenStorageHasNoData(t *testing.T) { + storage := &dashboardStorageStub{ + getLatestRequestDateFn: func() (*time.Time, error) { + return nil, nil + }, + } + req := httptest.NewRequest(http.MethodGet, "/api/latest-request-date", nil) + rr := httptest.NewRecorder() + + newTestHandler(storage).GetLatestRequestDate(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response struct { + LatestDate *time.Time `json:"latestDate"` + } + decodeJSONBody(t, rr, &response) + if response.LatestDate != nil { + t.Fatalf("expected latestDate to be null, got %#v", response.LatestDate) + } +} diff --git a/proxy/internal/handler/handlers_proxy_test.go b/proxy/internal/handler/handlers_proxy_test.go new file mode 100644 index 0000000..eda2ffc --- /dev/null +++ b/proxy/internal/handler/handlers_proxy_test.go @@ -0,0 +1,302 @@ +package handler + +import ( + "bytes" + "context" + "io" + "log" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/seifghazi/claude-code-monitor/internal/config" + "github.com/seifghazi/claude-code-monitor/internal/model" + "github.com/seifghazi/claude-code-monitor/internal/provider" +) + +type passthroughStorageStub struct { + saveRequestFn func(*model.RequestLog) (string, error) + updateRequestFn func(*model.RequestLog) error + getSettingsFn func() (*model.ProxySettings, error) + savedRequests int + updatedRequests int +} + +func (s *passthroughStorageStub) SaveRequest(request *model.RequestLog) (string, error) { + s.savedRequests++ + if s.saveRequestFn != nil { + return s.saveRequestFn(request) + } + return "req-1", nil +} +func (s *passthroughStorageStub) GetRequests(int, int, string) ([]model.RequestLog, int, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetAllRequests(string) ([]*model.RequestLog, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetRequestByShortID(string) (*model.RequestLog, string, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) ClearRequests() (int, error) { panic("unexpected call") } +func (s *passthroughStorageStub) UpdateRequestWithGrading(string, *model.PromptGrade) error { + panic("unexpected call") +} +func (s *passthroughStorageStub) UpdateRequestWithResponse(request *model.RequestLog) error { + s.updatedRequests++ + if s.updateRequestFn != nil { + return s.updateRequestFn(request) + } + return nil +} +func (s *passthroughStorageStub) DeleteRequestsOlderThan(time.Duration) (int, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetDatabaseStats() (map[string]interface{}, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetUsageStats(string, string, string, string) (*model.UsageStats, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetRequestsSummary(string) ([]*model.RequestSummary, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetRequestsSummaryPaginated(string, string, string, int, int) ([]*model.RequestSummary, int, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetStats(string, string, string) (*model.DashboardStats, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetHourlyStats(string, string, int, string) (*model.HourlyStatsResponse, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetModelStats(string, string, string) (*model.ModelStatsResponse, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetLatestRequestDate() (*time.Time, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetDistinctOrganizations() ([]string, error) { + panic("unexpected call") +} +func (s *passthroughStorageStub) GetSettings() (*model.ProxySettings, error) { + if s.getSettingsFn != nil { + return s.getSettingsFn() + } + return &model.ProxySettings{}, nil +} +func (s *passthroughStorageStub) SaveSettings(*model.ProxySettings) error { panic("unexpected call") } +func (s *passthroughStorageStub) GetConfig() *config.StorageConfig { return &config.StorageConfig{} } +func (s *passthroughStorageStub) EnsureDirectoryExists() error { return nil } +func (s *passthroughStorageStub) Close() error { return nil } + +type passthroughProviderStub struct { + forwardRequestFn func(context.Context, *http.Request) (*http.Response, error) +} + +func (p *passthroughProviderStub) Name() string { return "stub" } +func (p *passthroughProviderStub) ForwardRequest(ctx context.Context, req *http.Request) (*http.Response, error) { + if p.forwardRequestFn != nil { + return p.forwardRequestFn(ctx, req) + } + panic("unexpected call") +} + +var _ provider.Provider = (*passthroughProviderStub)(nil) + +func TestChatCompletionsReturnsHelpfulError(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil) + rr := httptest.NewRecorder() + + (&Handler{}).ChatCompletions(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d", rr.Code) + } + + var response model.ErrorResponse + decodeJSONBody(t, rr, &response) + if response.Error == "" { + t.Fatal("expected non-empty error message") + } +} + +func TestModelsReturnsEmptyList(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/v1/models", nil) + rr := httptest.NewRecorder() + + (&Handler{}).Models(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response model.ModelsResponse + decodeJSONBody(t, rr, &response) + if response.Object != "list" || len(response.Data) != 0 { + t.Fatalf("unexpected models response: %#v", response) + } +} + +func TestHealthReturnsHealthyStatus(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/health", nil) + rr := httptest.NewRecorder() + + (&Handler{}).Health(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + + var response model.HealthResponse + decodeJSONBody(t, rr, &response) + if response.Status != "healthy" { + t.Fatalf("unexpected health response: %#v", response) + } + if response.Timestamp.IsZero() { + t.Fatalf("expected non-zero timestamp: %#v", response) + } +} + +func TestOpenAPIEndpointsExposeDiscoveryFormats(t *testing.T) { + t.Run("json", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/openapi.json", nil) + rr := httptest.NewRecorder() + + (&Handler{}).OpenAPIJSON(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + if ct := rr.Header().Get("Content-Type"); ct != "application/json" { + t.Fatalf("expected json content type, got %q", ct) + } + if rr.Header().Get("Access-Control-Allow-Origin") != "*" { + t.Fatalf("expected CORS header, got %#v", rr.Header()) + } + + var payload map[string]interface{} + decodeJSONBody(t, rr, &payload) + if payload["openapi"] == nil || payload["paths"] == nil { + t.Fatalf("unexpected openapi json payload: %#v", payload) + } + }) + + t.Run("yaml", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/openapi.yaml", nil) + rr := httptest.NewRecorder() + + (&Handler{}).OpenAPIYAML(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + if ct := rr.Header().Get("Content-Type"); ct != "application/x-yaml" { + t.Fatalf("expected yaml content type, got %q", ct) + } + if body := rr.Body.String(); !bytes.Contains([]byte(body), []byte("openapi:")) { + t.Fatalf("expected yaml body to contain openapi header, got %q", body) + } + }) +} + +func TestProxyPassthroughForwardsResponseAndPersistsMetadata(t *testing.T) { + storage := &passthroughStorageStub{} + upstreamResponse := `{"ok":true}` + provider := &passthroughProviderStub{ + forwardRequestFn: func(ctx context.Context, req *http.Request) (*http.Response, error) { + if req.Header.Get("X-Added") != "from-test" { + t.Fatalf("expected request header rule to be applied, got headers %#v", req.Header) + } + + bodyBytes, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("failed reading forwarded request body: %v", err) + } + if string(bodyBytes) != `{"input":"hello"}` { + t.Fatalf("unexpected forwarded body %q", string(bodyBytes)) + } + + resp := &http.Response{ + StatusCode: http.StatusAccepted, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + "X-Upstream": []string{"yes"}, + "Connection": []string{"keep-alive"}, + "Anthropic-Organization-Id": []string{"org-123"}, + }, + Body: io.NopCloser(bytes.NewBufferString(upstreamResponse)), + } + return resp, nil + }, + } + + storage.saveRequestFn = func(request *model.RequestLog) (string, error) { + if request.Method != http.MethodPost || request.Endpoint != "/v1/quota" { + t.Fatalf("unexpected saved request metadata: %#v", request) + } + if request.ContentType != "application/json" { + t.Fatalf("unexpected content type %q", request.ContentType) + } + bodyMap, ok := request.Body.(map[string]interface{}) + if !ok || bodyMap["input"] != "hello" { + t.Fatalf("expected parsed request body, got %#v", request.Body) + } + if authValues := request.Headers["Authorization"]; len(authValues) != 1 || authValues[0] == "Bearer secret" || authValues[0] == "" { + t.Fatalf("expected sanitized authorization header, got %#v", request.Headers) + } + return "req-1", nil + } + storage.updateRequestFn = func(request *model.RequestLog) error { + if request.OrganizationID != "org-123" { + t.Fatalf("expected organization id extracted, got %#v", request) + } + if request.Response == nil || request.Response.StatusCode != http.StatusAccepted { + t.Fatalf("expected response metadata recorded, got %#v", request.Response) + } + if string(request.Response.Body) != upstreamResponse { + t.Fatalf("expected json response body stored, got %#v", request.Response) + } + if connectionValues := request.Response.Headers["Connection"]; len(connectionValues) != 0 { + t.Fatalf("expected sanitized stored headers to exclude hop-by-hop data, got %#v", request.Response.Headers) + } + return nil + } + + h := &Handler{ + storageService: storage, + anthropicProvider: provider, + logger: log.New(io.Discard, "", 0), + cachedSettings: &model.ProxySettings{ + RequestHeaderRules: []model.HeaderRule{{Header: "X-Added", Action: "set", Value: "from-test", Enabled: true}}, + ResponseHeaderRules: []model.HeaderRule{{Header: "X-Proxy", Action: "set", Value: "applied", Enabled: true}}, + }, + } + + req := httptest.NewRequest(http.MethodPost, "/v1/quota", bytes.NewBufferString(`{"input":"hello"}`)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer secret") + rr := httptest.NewRecorder() + + h.ProxyPassthrough(rr, req) + + if rr.Code != http.StatusAccepted { + t.Fatalf("expected 202, got %d", rr.Code) + } + if rr.Body.String() != upstreamResponse { + t.Fatalf("unexpected client body %q", rr.Body.String()) + } + if rr.Header().Get("X-Upstream") != "yes" { + t.Fatalf("expected upstream header to be forwarded, got %#v", rr.Header()) + } + if rr.Header().Get("X-Proxy") != "applied" { + t.Fatalf("expected response header rule to be applied, got %#v", rr.Header()) + } + if rr.Header().Get("Connection") != "" { + t.Fatalf("expected hop-by-hop header to be stripped, got %#v", rr.Header()) + } + if storage.savedRequests != 1 || storage.updatedRequests != 1 { + t.Fatalf("expected request save/update pair, got saves=%d updates=%d", storage.savedRequests, storage.updatedRequests) + } +} diff --git a/proxy/internal/handler/handlers_settings_test.go b/proxy/internal/handler/handlers_settings_test.go new file mode 100644 index 0000000..1d633db --- /dev/null +++ b/proxy/internal/handler/handlers_settings_test.go @@ -0,0 +1,209 @@ +package handler + +import ( + "bytes" + "errors" + "io" + "log" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/seifghazi/claude-code-monitor/internal/config" + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +type stubStorage struct { + getSettingsFn func() (*model.ProxySettings, error) + saveSettingsFn func(settings *model.ProxySettings) error + getCalls int + saveCalls int +} + +func (s *stubStorage) SaveRequest(*model.RequestLog) (string, error) { panic("unexpected call") } +func (s *stubStorage) GetRequests(int, int, string) ([]model.RequestLog, int, error) { + panic("unexpected call") +} +func (s *stubStorage) GetAllRequests(string) ([]*model.RequestLog, error) { panic("unexpected call") } +func (s *stubStorage) GetRequestByShortID(string) (*model.RequestLog, string, error) { + panic("unexpected call") +} +func (s *stubStorage) ClearRequests() (int, error) { panic("unexpected call") } +func (s *stubStorage) UpdateRequestWithGrading(string, *model.PromptGrade) error { + panic("unexpected call") +} +func (s *stubStorage) UpdateRequestWithResponse(*model.RequestLog) error { panic("unexpected call") } +func (s *stubStorage) DeleteRequestsOlderThan(time.Duration) (int, error) { panic("unexpected call") } +func (s *stubStorage) GetDatabaseStats() (map[string]interface{}, error) { panic("unexpected call") } +func (s *stubStorage) GetUsageStats(string, string, string, string) (*model.UsageStats, error) { + panic("unexpected call") +} +func (s *stubStorage) GetRequestsSummary(string) ([]*model.RequestSummary, error) { + panic("unexpected call") +} +func (s *stubStorage) GetRequestsSummaryPaginated(string, string, string, int, int) ([]*model.RequestSummary, int, error) { + panic("unexpected call") +} +func (s *stubStorage) GetStats(string, string, string) (*model.DashboardStats, error) { + panic("unexpected call") +} +func (s *stubStorage) GetHourlyStats(string, string, int, string) (*model.HourlyStatsResponse, error) { + panic("unexpected call") +} +func (s *stubStorage) GetModelStats(string, string, string) (*model.ModelStatsResponse, error) { + panic("unexpected call") +} +func (s *stubStorage) GetLatestRequestDate() (*time.Time, error) { panic("unexpected call") } +func (s *stubStorage) GetDistinctOrganizations() ([]string, error) { panic("unexpected call") } +func (s *stubStorage) GetSettings() (*model.ProxySettings, error) { + s.getCalls++ + if s.getSettingsFn != nil { + return s.getSettingsFn() + } + return &model.ProxySettings{}, nil +} +func (s *stubStorage) SaveSettings(settings *model.ProxySettings) error { + s.saveCalls++ + if s.saveSettingsFn != nil { + return s.saveSettingsFn(settings) + } + return nil +} +func (s *stubStorage) GetConfig() *config.StorageConfig { return &config.StorageConfig{} } +func (s *stubStorage) EnsureDirectoryExists() error { return nil } +func (s *stubStorage) Close() error { return nil } + +func TestGetCachedSettingsCachesFirstLoad(t *testing.T) { + storage := &stubStorage{ + getSettingsFn: func() (*model.ProxySettings, error) { + return &model.ProxySettings{ + RequestHeaderRules: []model.HeaderRule{{Header: "X-Test", Action: "set", Value: "1", Enabled: true}}, + }, nil + }, + } + h := &Handler{ + storageService: storage, + logger: log.New(io.Discard, "", 0), + } + + first := h.GetCachedSettings() + second := h.GetCachedSettings() + + if storage.getCalls != 1 { + t.Fatalf("expected one storage read, got %d", storage.getCalls) + } + if len(first.RequestHeaderRules) != 1 || len(second.RequestHeaderRules) != 1 { + t.Fatalf("expected cached settings to be returned, got %#v %#v", first, second) + } +} + +func TestSaveSettingsUpdatesCache(t *testing.T) { + storage := &stubStorage{} + h := &Handler{ + storageService: storage, + logger: log.New(io.Discard, "", 0), + } + + body := []byte(`{"requestHeaderRules":[{"header":"X-New","action":"set","value":"abc","enabled":true}]}`) + req := httptest.NewRequest(http.MethodPut, "/api/settings", bytes.NewReader(body)) + rr := httptest.NewRecorder() + + h.SaveSettings(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } + if storage.saveCalls != 1 { + t.Fatalf("expected one settings save, got %d", storage.saveCalls) + } + + cached := h.GetCachedSettings() + if storage.getCalls != 0 { + t.Fatalf("expected cached settings to avoid storage read, got %d reads", storage.getCalls) + } + if len(cached.RequestHeaderRules) != 1 || cached.RequestHeaderRules[0].Header != "X-New" { + t.Fatalf("expected cache updated from save, got %#v", cached) + } +} + +func TestGetSettingsReturnsStorageError(t *testing.T) { + storage := &stubStorage{ + getSettingsFn: func() (*model.ProxySettings, error) { + return nil, errors.New("boom") + }, + } + h := &Handler{ + storageService: storage, + logger: log.New(io.Discard, "", 0), + } + + req := httptest.NewRequest(http.MethodGet, "/api/settings", nil) + rr := httptest.NewRecorder() + + h.GetSettings(rr, req) + + if rr.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", rr.Code) + } + + var response model.ErrorResponse + decodeJSONBody(t, rr, &response) + if response.Error != "Failed to get settings" { + t.Fatalf("unexpected error response: %#v", response) + } +} + +func TestSaveSettingsRejectsInvalidBodyAndStorageFailures(t *testing.T) { + t.Run("invalid json", func(t *testing.T) { + h := &Handler{ + storageService: &stubStorage{}, + logger: log.New(io.Discard, "", 0), + } + + req := httptest.NewRequest(http.MethodPut, "/api/settings", bytes.NewBufferString(`{"requestHeaderRules":`)) + rr := httptest.NewRecorder() + + h.SaveSettings(rr, req) + + if rr.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d", rr.Code) + } + + var response model.ErrorResponse + decodeJSONBody(t, rr, &response) + if response.Error != "Invalid request body" { + t.Fatalf("unexpected error response: %#v", response) + } + }) + + t.Run("storage error", func(t *testing.T) { + storage := &stubStorage{ + saveSettingsFn: func(settings *model.ProxySettings) error { + return errors.New("boom") + }, + } + h := &Handler{ + storageService: storage, + logger: log.New(io.Discard, "", 0), + } + + req := httptest.NewRequest(http.MethodPut, "/api/settings", bytes.NewBufferString(`{"responseHeaderRules":[{"header":"X-Test","action":"set","value":"1","enabled":true}]}`)) + rr := httptest.NewRecorder() + + h.SaveSettings(rr, req) + + if rr.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d", rr.Code) + } + if storage.saveCalls != 1 { + t.Fatalf("expected one settings save attempt, got %d", storage.saveCalls) + } + + var response model.ErrorResponse + decodeJSONBody(t, rr, &response) + if response.Error != "Failed to save settings" { + t.Fatalf("unexpected error response: %#v", response) + } + }) +} diff --git a/proxy/internal/handler/openapi.go b/proxy/internal/handler/openapi.go new file mode 100644 index 0000000..2ab1600 --- /dev/null +++ b/proxy/internal/handler/openapi.go @@ -0,0 +1,900 @@ +package handler + +import ( + "encoding/json" + "net/http" + + "gopkg.in/yaml.v3" +) + +// openAPISpec is the embedded OpenAPI 3.0 specification for the proxy API. +var openAPISpec = ` +openapi: "3.0.3" +info: + title: Claude Code Proxy API + description: | + An Anthropic API proxy that provides request logging, model routing, usage + analytics, and a dashboard UI. The proxy exposes two groups of endpoints: + + **Proxy endpoints** – drop-in replacements for the upstream Anthropic API. + Point your Claude Code (or any Anthropic SDK client) at this proxy and all + requests are forwarded, logged, and optionally re-routed to a different model + or provider. + + **Dashboard endpoints** – read-only analytics and configuration APIs that + power the built-in web dashboard. These are protected by HTTP Basic Auth + when DASHBOARD_PASSWORD is set. + version: "1.0.0" + contact: + name: Claude Code Proxy + license: + name: MIT + +servers: + - url: / + description: This proxy instance + +tags: + - name: proxy + description: | + Drop-in Anthropic API proxy endpoints. Authenticate with the same + x-api-key / Authorization header you use for the upstream Anthropic API. + - name: dashboard + description: | + Analytics and configuration endpoints for the web dashboard. + Protected by DASHBOARD_PASSWORD basic auth when configured. + - name: health + description: Health and discovery endpoints (no auth required). + +paths: + # ── Proxy endpoints ──────────────────────────────────────────────────── + /v1/messages: + post: + operationId: createMessage + tags: [proxy] + summary: Create a message (Anthropic Messages API) + description: | + Forwards the request to the upstream Anthropic (or routed) provider. + Supports both streaming (SSE) and non-streaming responses. The proxy + logs the request/response, applies any configured model routing rules + and header rules, then returns the upstream response verbatim. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AnthropicRequest" + responses: + "200": + description: Successful message response (non-streaming) + content: + application/json: + schema: + $ref: "#/components/schemas/AnthropicResponse" + text/event-stream: + schema: + type: string + description: SSE stream of Anthropic streaming events + "400": + description: Invalid request + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + "500": + description: Upstream or internal error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /v1/chat/completions: + post: + operationId: chatCompletions + tags: [proxy] + summary: Chat completions (OpenAI-compatible – not supported) + description: | + Returns a 400 error directing callers to use /v1/messages instead. + This endpoint exists for compatibility detection only. + requestBody: + content: + application/json: + schema: + type: object + responses: + "400": + description: Not supported – use /v1/messages + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + /v1/models: + get: + operationId: listModels + tags: [proxy] + summary: List available models + description: | + Returns the list of models known to the proxy. The proxy uses + pattern-based routing so any model accepted by the upstream provider + will work; this endpoint currently returns an empty list. + responses: + "200": + description: Model list + content: + application/json: + schema: + $ref: "#/components/schemas/ModelsResponse" + + # ── Health & discovery ───────────────────────────────────────────────── + /health: + get: + operationId: healthCheck + tags: [health] + summary: Health check (binary up/ready signal for load balancers) + description: | + Returns 200 with status=healthy while the process is accepting + traffic, and 503 with status=draining once a SIGTERM has been + received. Traefik (or any LB doing health-based routing) should + treat 503 as "stop sending new requests to this backend", which is + the signal the graceful-drain loop relies on. + responses: + "200": + description: Service is healthy + content: + application/json: + schema: + $ref: "#/components/schemas/HealthResponse" + "503": + description: Service is draining (SIGTERM received). Stop routing here. + content: + application/json: + schema: + $ref: "#/components/schemas/DrainingResponse" + + /livez: + get: + operationId: livenessProbe + tags: [health] + summary: Live operational state (in-flight gauge + draining flag) + description: | + Always returns 200 with the current in-flight request count and + draining flag. Distinct from /health, which is a binary up/ready + signal — /livez is for observability and deploy-time orchestration + ("how many requests are still active before I cycle this slot?"). + responses: + "200": + description: Operational state + content: + application/json: + schema: + $ref: "#/components/schemas/LivezResponse" + + /openapi.json: + get: + operationId: getOpenAPISpec + tags: [health] + summary: OpenAPI specification (JSON) + responses: + "200": + description: The OpenAPI 3.0 spec for this API + content: + application/json: + schema: + type: object + + /openapi.yaml: + get: + operationId: getOpenAPISpecYAML + tags: [health] + summary: OpenAPI specification (YAML) + responses: + "200": + description: The OpenAPI 3.0 spec for this API + content: + application/x-yaml: + schema: + type: string + + # ── Dashboard endpoints ──────────────────────────────────────────────── + /api/requests: + get: + operationId: getRequests + tags: [dashboard] + summary: List logged requests + parameters: + - name: page + in: query + schema: { type: integer, default: 1 } + - name: limit + in: query + schema: { type: integer, default: 10 } + - name: model + in: query + schema: { type: string, default: "all" } + description: Filter by model name (substring match) or "all" + responses: + "200": + description: Paginated request list + content: + application/json: + schema: + type: object + properties: + requests: + type: array + items: + $ref: "#/components/schemas/RequestLog" + total: + type: integer + delete: + operationId: deleteRequests + tags: [dashboard] + summary: Clear all logged requests + responses: + "200": + description: Requests cleared + content: + application/json: + schema: + type: object + properties: + message: { type: string } + deleted: { type: integer } + + /api/requests/summary: + get: + operationId: getRequestsSummary + tags: [dashboard] + summary: Lightweight request summaries for fast list rendering + parameters: + - name: model + in: query + schema: { type: string, default: "all" } + - name: start + in: query + schema: { type: string, format: date-time } + description: Start of time range (UTC ISO 8601) + - name: end + in: query + schema: { type: string, format: date-time } + description: End of time range (UTC ISO 8601) + - name: offset + in: query + schema: { type: integer, default: 0 } + - name: limit + in: query + schema: { type: integer, default: 0 } + description: Max results (0 = unlimited) + responses: + "200": + description: Paginated request summaries + content: + application/json: + schema: + type: object + properties: + requests: + type: array + items: + $ref: "#/components/schemas/RequestSummary" + total: { type: integer } + offset: { type: integer } + limit: { type: integer } + + /api/requests/latest-date: + get: + operationId: getLatestRequestDate + tags: [dashboard] + summary: Date of the most recent logged request + responses: + "200": + content: + application/json: + schema: + type: object + properties: + latestDate: { type: string, format: date-time } + + /api/requests/{id}: + get: + operationId: getRequestByID + tags: [dashboard] + summary: Get a single request by ID + parameters: + - name: id + in: path + required: true + schema: { type: string } + description: Short or full request ID + responses: + "200": + content: + application/json: + schema: + type: object + properties: + request: + $ref: "#/components/schemas/RequestLog" + fullId: { type: string } + "404": + description: Request not found + + /api/stats: + get: + operationId: getStats + tags: [dashboard] + summary: Aggregated usage statistics + parameters: + - name: start_date + in: query + schema: { type: string } + - name: end_date + in: query + schema: { type: string } + - name: model + in: query + schema: { type: string } + - name: org + in: query + schema: { type: string } + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/UsageStats" + + /api/stats/dashboard: + get: + operationId: getDashboardStats + tags: [dashboard] + summary: Daily token usage for dashboard charts + parameters: + - name: start + in: query + schema: { type: string, format: date-time } + - name: end + in: query + schema: { type: string, format: date-time } + - name: org + in: query + schema: { type: string } + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/DashboardStats" + + /api/stats/hourly: + get: + operationId: getHourlyStats + tags: [dashboard] + summary: Hourly token usage breakdown + parameters: + - name: start + in: query + required: true + schema: { type: string, format: date-time } + - name: end + in: query + required: true + schema: { type: string, format: date-time } + - name: bucket + in: query + schema: { type: integer, default: 60 } + description: Bucket size in minutes + - name: org + in: query + schema: { type: string } + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/HourlyStatsResponse" + + /api/stats/models: + get: + operationId: getModelStats + tags: [dashboard] + summary: Per-model token usage breakdown + parameters: + - name: start + in: query + required: true + schema: { type: string, format: date-time } + - name: end + in: query + required: true + schema: { type: string, format: date-time } + - name: org + in: query + schema: { type: string } + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ModelStatsResponse" + + /api/stats/organizations: + get: + operationId: getOrganizations + tags: [dashboard] + summary: List distinct organization IDs + responses: + "200": + content: + application/json: + schema: + type: object + properties: + organizations: + type: array + items: { type: string } + + /api/conversations: + get: + operationId: getConversations + tags: [dashboard] + summary: List conversations (grouped by session) + parameters: + - name: model + in: query + schema: { type: string, default: "all" } + - name: page + in: query + schema: { type: integer, default: 1 } + - name: limit + in: query + schema: { type: integer, default: 10 } + responses: + "200": + content: + application/json: + schema: + type: object + properties: + conversations: + type: array + items: + type: object + properties: + id: { type: string } + requestCount: { type: integer } + startTime: { type: string, format: date-time } + lastActivity: { type: string, format: date-time } + duration: { type: integer, description: "Duration in ms" } + firstMessage: { type: string } + projectPath: { type: string } + projectName: { type: string } + model: { type: string } + hasMore: { type: boolean } + total: { type: integer } + page: { type: integer } + limit: { type: integer } + + /api/conversations/{id}: + get: + operationId: getConversationByID + tags: [dashboard] + summary: Get a single conversation by session ID + parameters: + - name: id + in: path + required: true + schema: { type: string } + - name: project + in: query + required: true + schema: { type: string } + description: Project path the conversation belongs to + responses: + "200": + content: + application/json: + schema: + type: object + "404": + description: Conversation not found + + /api/conversations/project: + get: + operationId: getConversationsByProject + tags: [dashboard] + summary: List conversations for a specific project + parameters: + - name: project + in: query + required: true + schema: { type: string } + responses: + "200": + content: + application/json: + schema: + type: object + + /api/settings: + get: + operationId: getSettings + tags: [dashboard] + summary: Get current proxy settings + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ProxySettings" + put: + operationId: saveSettings + tags: [dashboard] + summary: Update proxy settings + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ProxySettings" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/ProxySettings" + +components: + securitySchemes: + apiKey: + type: apiKey + in: header + name: x-api-key + description: Anthropic API key (forwarded to upstream) + bearerAuth: + type: http + scheme: bearer + description: Bearer token authentication + dashboardBasicAuth: + type: http + scheme: basic + description: Dashboard password (username is ignored) + + schemas: + ErrorResponse: + type: object + properties: + error: { type: string } + details: { type: string } + + HealthResponse: + type: object + properties: + status: { type: string, example: "healthy" } + timestamp: { type: string, format: date-time } + + DrainingResponse: + type: object + properties: + status: { type: string, example: "draining" } + timestamp: { type: string, format: date-time } + in_flight: { type: integer, example: 3 } + + LivezResponse: + type: object + properties: + status: { type: string, example: "ok" } + timestamp: { type: string, format: date-time } + in_flight: { type: integer, example: 0 } + draining: { type: boolean, example: false } + + AnthropicRequest: + type: object + required: [model, messages, max_tokens] + properties: + model: + type: string + description: | + Model ID to use. The proxy may re-route this to a different + model/provider based on configured routing rules. + example: "claude-sonnet-4-5-20250514" + messages: + type: array + items: + $ref: "#/components/schemas/AnthropicMessage" + max_tokens: + type: integer + example: 1024 + temperature: + type: number + format: float + system: + type: array + items: + $ref: "#/components/schemas/SystemMessage" + stream: + type: boolean + default: false + tools: + type: array + items: + $ref: "#/components/schemas/Tool" + tool_choice: + description: Tool choice configuration + + AnthropicMessage: + type: object + required: [role, content] + properties: + role: + type: string + enum: [user, assistant] + content: + description: String or array of content blocks + oneOf: + - type: string + - type: array + items: + type: object + properties: + type: { type: string } + text: { type: string } + + SystemMessage: + type: object + properties: + type: { type: string, example: "text" } + text: { type: string } + cache_control: + type: object + properties: + type: { type: string, example: "ephemeral" } + + Tool: + type: object + properties: + name: { type: string } + description: { type: string } + input_schema: + type: object + properties: + type: {} + properties: { type: object } + required: + type: array + items: { type: string } + + AnthropicResponse: + type: object + properties: + id: { type: string } + type: { type: string, example: "message" } + role: { type: string, example: "assistant" } + model: { type: string } + stop_reason: { type: string } + stop_sequence: { type: string, nullable: true } + content: + type: array + items: + type: object + properties: + type: { type: string } + text: { type: string } + usage: + $ref: "#/components/schemas/AnthropicUsage" + + AnthropicUsage: + type: object + properties: + input_tokens: { type: integer } + output_tokens: { type: integer } + cache_creation_input_tokens: { type: integer } + cache_read_input_tokens: { type: integer } + service_tier: { type: string } + + ModelsResponse: + type: object + properties: + object: { type: string, example: "list" } + data: + type: array + items: + type: object + properties: + id: { type: string } + object: { type: string } + created: { type: integer } + owned_by: { type: string } + + RequestLog: + type: object + properties: + requestId: { type: string } + timestamp: { type: string, format: date-time } + method: { type: string } + endpoint: { type: string } + model: { type: string } + originalModel: { type: string } + routedModel: { type: string } + userAgent: { type: string } + contentType: { type: string } + conversationHash: { type: string } + messageCount: { type: integer } + organizationId: { type: string } + response: + $ref: "#/components/schemas/ResponseLog" + + ResponseLog: + type: object + properties: + statusCode: { type: integer } + responseTime: { type: integer, description: "Response time in ms" } + isStreaming: { type: boolean } + completedAt: { type: string, format: date-time } + streamError: { type: string } + rateLimit: + $ref: "#/components/schemas/RateLimitInfo" + + RateLimitInfo: + type: object + properties: + organizationId: { type: string } + requestsLimit: { type: integer } + requestsRemaining: { type: integer } + requestsReset: { type: string } + tokensLimit: { type: integer } + tokensRemaining: { type: integer } + tokensReset: { type: string } + unifiedStatus: { type: string } + unifiedUtilization5h: { type: number } + unifiedReset5h: { type: string } + unifiedUtilization7d: { type: number } + unifiedReset7d: { type: string } + + RequestSummary: + type: object + properties: + requestId: { type: string } + timestamp: { type: string, format: date-time } + method: { type: string } + endpoint: { type: string } + model: { type: string } + originalModel: { type: string } + routedModel: { type: string } + statusCode: { type: integer } + responseTime: { type: integer } + usage: + $ref: "#/components/schemas/AnthropicUsage" + conversationHash: { type: string } + messageCount: { type: integer } + stopReason: { type: string } + + UsageStats: + type: object + properties: + total_requests: { type: integer } + total_input_tokens: { type: integer, format: int64 } + total_output_tokens: { type: integer, format: int64 } + total_cache_tokens: { type: integer, format: int64 } + requests_by_model: + type: object + additionalProperties: + type: object + properties: + request_count: { type: integer } + input_tokens: { type: integer, format: int64 } + output_tokens: { type: integer, format: int64 } + cache_tokens: { type: integer, format: int64 } + start_date: { type: string } + end_date: { type: string } + + DashboardStats: + type: object + properties: + dailyStats: + type: array + items: + type: object + properties: + date: { type: string } + tokens: { type: integer, format: int64 } + requests: { type: integer } + + HourlyStatsResponse: + type: object + properties: + hourlyStats: + type: array + items: + type: object + properties: + hour: { type: integer } + label: { type: string } + tokens: { type: integer, format: int64 } + requests: { type: integer } + todayTokens: { type: integer, format: int64 } + todayRequests: { type: integer } + avgResponseTime: { type: integer, format: int64 } + + ModelStatsResponse: + type: object + properties: + modelStats: + type: array + items: + type: object + properties: + model: { type: string } + tokens: { type: integer, format: int64 } + requests: { type: integer } + + ProxySettings: + type: object + properties: + requestHeaderRules: + type: array + items: + $ref: "#/components/schemas/HeaderRule" + responseHeaderRules: + type: array + items: + $ref: "#/components/schemas/HeaderRule" + + HeaderRule: + type: object + properties: + header: { type: string, description: "Header name (case-insensitive)" } + action: + type: string + enum: [block, set, replace] + value: { type: string } + find: { type: string, description: "For replace action: string to find" } + enabled: { type: boolean } + +security: + - apiKey: [] + - bearerAuth: [] +` + +// OpenAPIJSON serves the OpenAPI spec as JSON. +func (h *Handler) OpenAPIJSON(w http.ResponseWriter, r *http.Request) { + var spec interface{} + if err := yaml.Unmarshal([]byte(openAPISpec), &spec); err != nil { + writeErrorResponse(w, "Failed to parse OpenAPI spec", http.StatusInternalServerError) + return + } + spec = convertYAMLToJSON(spec) + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + json.NewEncoder(w).Encode(spec) +} + +// OpenAPIYAML serves the OpenAPI spec as YAML. +func (h *Handler) OpenAPIYAML(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/x-yaml") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Write([]byte(openAPISpec)) +} + +// convertYAMLToJSON recursively converts map[string]interface{} (from yaml) to +// JSON-compatible types. yaml.v3 uses map[string]interface{} by default so this +// mainly handles nested maps. +func convertYAMLToJSON(v interface{}) interface{} { + switch val := v.(type) { + case map[string]interface{}: + out := make(map[string]interface{}, len(val)) + for k, v2 := range val { + out[k] = convertYAMLToJSON(v2) + } + return out + case []interface{}: + out := make([]interface{}, len(val)) + for i, v2 := range val { + out[i] = convertYAMLToJSON(v2) + } + return out + default: + return v + } +} diff --git a/proxy/internal/handler/utils.go b/proxy/internal/handler/utils.go index 571f9f1..c5b2ee0 100644 --- a/proxy/internal/handler/utils.go +++ b/proxy/internal/handler/utils.go @@ -2,67 +2,60 @@ package handler import ( "crypto/sha256" - "encoding/json" "fmt" "net/http" + "strconv" "strings" - "time" "github.com/seifghazi/claude-code-monitor/internal/model" ) -// Headers that should be forwarded from upstream responses to clients -var forwardableResponseHeaders = []string{ - // Anthropic rate limit headers - "anthropic-ratelimit-requests-limit", - "anthropic-ratelimit-requests-remaining", - "anthropic-ratelimit-requests-reset", - "anthropic-ratelimit-tokens-limit", - "anthropic-ratelimit-tokens-remaining", - "anthropic-ratelimit-tokens-reset", - // Standard rate limit headers - "x-ratelimit-limit", - "x-ratelimit-remaining", - "x-ratelimit-reset", - "retry-after", - // Request tracking - "x-request-id", - "request-id", - // Anthropic specific - "anthropic-organization-id", - // OpenAI specific - "openai-organization", - "openai-processing-ms", - "openai-version", - "x-request-id", +var hopByHopHeaders = map[string]bool{ + "connection": true, + "keep-alive": true, + "proxy-authenticate": true, + "proxy-authorization": true, + "te": true, + "trailers": true, + "transfer-encoding": true, + "upgrade": true, + "content-encoding": true, // We handle decompression ourselves + "content-length": true, // May change after decompression } -// ForwardResponseHeaders copies important headers from upstream response to client response -func ForwardResponseHeaders(w http.ResponseWriter, resp *http.Response) { - for _, header := range forwardableResponseHeaders { - if values := resp.Header.Values(header); len(values) > 0 { - for _, value := range values { - w.Header().Add(header, value) +// ApplyHeaderRules applies block/set/replace rules to an http.Header in-place. +func ApplyHeaderRules(headers http.Header, rules []model.HeaderRule) { + for _, rule := range rules { + if !rule.Enabled { + continue + } + key := http.CanonicalHeaderKey(rule.Header) + switch rule.Action { + case "block": + headers.Del(key) + case "set": + headers.Set(key, rule.Value) + case "replace": + if rule.Find == "" { + continue + } + for i, v := range headers.Values(key) { + if strings.Contains(v, rule.Find) { + replaced := strings.ReplaceAll(v, rule.Find, rule.Value) + if i == 0 { + headers.Set(key, replaced) + } else { + headers.Add(key, replaced) + } + } } } } } -// CopyAllResponseHeaders copies all non-hop-by-hop headers from upstream to client +// CopyAllResponseHeaders forwards all upstream response headers to the client, +// stripping only hop-by-hop headers that must not be forwarded by a proxy. func CopyAllResponseHeaders(w http.ResponseWriter, resp *http.Response) { - hopByHopHeaders := map[string]bool{ - "connection": true, - "keep-alive": true, - "proxy-authenticate": true, - "proxy-authorization": true, - "te": true, - "trailers": true, - "transfer-encoding": true, - "upgrade": true, - "content-encoding": true, // We handle decompression ourselves - "content-length": true, // May change after decompression - } - for key, values := range resp.Header { if hopByHopHeaders[strings.ToLower(key)] { continue @@ -73,6 +66,115 @@ func CopyAllResponseHeaders(w http.ResponseWriter, resp *http.Response) { } } +// SanitizeResponseHeaders strips hop-by-hop proxy headers before applying the +// generic sensitive-header sanitization used for stored metadata. +func SanitizeResponseHeaders(headers http.Header) http.Header { + filtered := make(http.Header) + + for key, values := range headers { + if hopByHopHeaders[strings.ToLower(key)] { + continue + } + copiedValues := append([]string(nil), values...) + filtered[key] = copiedValues + } + + return SanitizeHeaders(filtered) +} + +// ExtractRateLimitInfo parses rate limit headers from the upstream response +func ExtractRateLimitInfo(headers http.Header) *model.RateLimitInfo { + info := &model.RateLimitInfo{} + found := false + + // Organization ID + if v := headers.Get("anthropic-organization-id"); v != "" { + info.OrganizationID = v + found = true + } + + // Unified quota system (current Anthropic model) + if v := headers.Get("anthropic-ratelimit-unified-status"); v != "" { + info.UnifiedStatus = v + found = true + } + if v := headers.Get("anthropic-ratelimit-unified-5h-utilization"); v != "" { + info.UnifiedUtilization5h, _ = strconv.ParseFloat(v, 64) + found = true + } + if v := headers.Get("anthropic-ratelimit-unified-5h-reset"); v != "" { + info.UnifiedReset5h = v + found = true + } + if v := headers.Get("anthropic-ratelimit-unified-7d-utilization"); v != "" { + info.UnifiedUtilization7d, _ = strconv.ParseFloat(v, 64) + found = true + } + if v := headers.Get("anthropic-ratelimit-unified-7d-reset"); v != "" { + info.UnifiedReset7d = v + found = true + } + if v := headers.Get("anthropic-ratelimit-unified-fallback-percentage"); v != "" { + info.UnifiedFallbackPercentage, _ = strconv.ParseFloat(v, 64) + found = true + } + if v := headers.Get("anthropic-ratelimit-unified-overage-status"); v != "" { + info.UnifiedOverageStatus = v + found = true + } + if v := headers.Get("anthropic-ratelimit-unified-representative-claim"); v != "" { + info.UnifiedRepresentativeClaim = v + found = true + } + + // Legacy per-resource rate limits + if v := headers.Get("anthropic-ratelimit-requests-limit"); v != "" { + info.RequestsLimit, _ = strconv.Atoi(v) + found = true + } + if v := headers.Get("anthropic-ratelimit-requests-remaining"); v != "" { + info.RequestsRemaining, _ = strconv.Atoi(v) + found = true + } + if v := headers.Get("anthropic-ratelimit-requests-reset"); v != "" { + info.RequestsReset = v + found = true + } + if v := headers.Get("anthropic-ratelimit-tokens-limit"); v != "" { + info.TokensLimit, _ = strconv.Atoi(v) + found = true + } + if v := headers.Get("anthropic-ratelimit-tokens-remaining"); v != "" { + info.TokensRemaining, _ = strconv.Atoi(v) + found = true + } + if v := headers.Get("anthropic-ratelimit-tokens-reset"); v != "" { + info.TokensReset = v + found = true + } + + // Fall back to standard rate limit headers + if !found { + if v := headers.Get("x-ratelimit-limit"); v != "" { + info.RequestsLimit, _ = strconv.Atoi(v) + found = true + } + if v := headers.Get("x-ratelimit-remaining"); v != "" { + info.RequestsRemaining, _ = strconv.Atoi(v) + found = true + } + if v := headers.Get("x-ratelimit-reset"); v != "" { + info.RequestsReset = v + found = true + } + } + + if !found { + return nil + } + return info +} + // SanitizeHeaders removes sensitive headers before logging/storage func SanitizeHeaders(headers http.Header) http.Header { sanitized := make(http.Header) @@ -112,222 +214,3 @@ func SanitizeHeaders(headers http.Header) http.Header { return sanitized } - -// ConversationDiffAnalyzer analyzes conversation flows to identify new vs repeated content -type ConversationDiffAnalyzer struct{} - -// NewConversationDiffAnalyzer creates a new conversation diff analyzer -func NewConversationDiffAnalyzer() *ConversationDiffAnalyzer { - return &ConversationDiffAnalyzer{} -} - -// ConversationFlowData represents the flow analysis of a conversation -type ConversationFlowData struct { - TotalMessages int `json:"totalMessages"` - NewMessages []int `json:"newMessages"` // Indices of new messages - DuplicateMessages []int `json:"duplicateMessages"` // Indices of duplicate messages - MessageHashes []string `json:"messageHashes"` // Content hashes for deduplication - ConversationHash string `json:"conversationHash"` // Hash of entire conversation - PreviousHash string `json:"previousHash"` // Hash of previous conversation state - Changes []ConversationChange `json:"changes"` // Detailed changes - FlowMetadata map[string]interface{} `json:"flowMetadata"` // Additional metadata -} - -// ConversationChange represents a specific change in the conversation -type ConversationChange struct { - Type string `json:"type"` // "added", "modified", "context" - MessageIdx int `json:"messageIdx"` // Index of the message - Role string `json:"role"` // Role of the message - ContentHash string `json:"contentHash"` // Hash of the content - Preview string `json:"preview"` // Short preview of content - Timestamp string `json:"timestamp"` // When this change was detected -} - -// AnalyzeConversationFlow analyzes a conversation to identify what's new vs repeated -func (c *ConversationDiffAnalyzer) AnalyzeConversationFlow(messages []model.AnthropicMessage, previousConversation []model.AnthropicMessage) *ConversationFlowData { - totalMessages := len(messages) - - // Create hashes for current conversation - currentHashes := make([]string, totalMessages) - for i, msg := range messages { - currentHashes[i] = c.hashMessage(msg) - } - - // Create hashes for previous conversation (if any) - var previousHashes []string - if previousConversation != nil { - previousHashes = make([]string, len(previousConversation)) - for i, msg := range previousConversation { - previousHashes[i] = c.hashMessage(msg) - } - } - - // Identify new vs duplicate messages - newMessages := []int{} - duplicateMessages := []int{} - changes := []ConversationChange{} - - // Simple approach: messages that appear after the previous conversation length are new - previousLength := len(previousHashes) - - for i, msg := range messages { - isNew := i >= previousLength - - // More sophisticated check: compare hashes - if !isNew && i < len(previousHashes) { - isNew = currentHashes[i] != previousHashes[i] - } - - if isNew { - newMessages = append(newMessages, i) - changes = append(changes, ConversationChange{ - Type: "added", - MessageIdx: i, - Role: msg.Role, - ContentHash: currentHashes[i], - Preview: c.getMessagePreview(msg), - Timestamp: fmt.Sprintf("%d", time.Now().Unix()), - }) - } else { - duplicateMessages = append(duplicateMessages, i) - changes = append(changes, ConversationChange{ - Type: "context", - MessageIdx: i, - Role: msg.Role, - ContentHash: currentHashes[i], - Preview: c.getMessagePreview(msg), - Timestamp: fmt.Sprintf("%d", time.Now().Unix()), - }) - } - } - - // If no previous conversation, consider a reasonable threshold of "new" vs "context" - if previousConversation == nil && totalMessages > 1 { - // Heuristic: last 30% of messages are "new", rest is context - newThreshold := max(1, int(float64(totalMessages)*0.3)) - contextEnd := totalMessages - newThreshold - - newMessages = []int{} - duplicateMessages = []int{} - changes = []ConversationChange{} - - for i := 0; i < totalMessages; i++ { - if i >= contextEnd { - newMessages = append(newMessages, i) - changes = append(changes, ConversationChange{ - Type: "added", - MessageIdx: i, - Role: messages[i].Role, - ContentHash: currentHashes[i], - Preview: c.getMessagePreview(messages[i]), - Timestamp: fmt.Sprintf("%d", time.Now().Unix()), - }) - } else { - duplicateMessages = append(duplicateMessages, i) - changes = append(changes, ConversationChange{ - Type: "context", - MessageIdx: i, - Role: messages[i].Role, - ContentHash: currentHashes[i], - Preview: c.getMessagePreview(messages[i]), - Timestamp: fmt.Sprintf("%d", time.Now().Unix()), - }) - } - } - } - - // Generate conversation hashes - conversationHash := c.hashConversation(messages) - previousHash := "" - if previousConversation != nil { - previousHash = c.hashConversation(previousConversation) - } - - return &ConversationFlowData{ - TotalMessages: totalMessages, - NewMessages: newMessages, - DuplicateMessages: duplicateMessages, - MessageHashes: currentHashes, - ConversationHash: conversationHash, - PreviousHash: previousHash, - Changes: changes, - FlowMetadata: map[string]interface{}{ - "newCount": len(newMessages), - "duplicateCount": len(duplicateMessages), - "analyzeTime": time.Now().Format(time.RFC3339), - }, - } -} - -// hashMessage creates a hash of a message for deduplication -func (c *ConversationDiffAnalyzer) hashMessage(msg model.AnthropicMessage) string { - // Create a stable representation of the message - content := c.normalizeMessageContent(msg.Content) - data := fmt.Sprintf("%s|%s", msg.Role, content) - - hash := sha256.Sum256([]byte(data)) - return fmt.Sprintf("%x", hash[:8]) // Use first 8 bytes for shorter hash -} - -// hashConversation creates a hash of the entire conversation -func (c *ConversationDiffAnalyzer) hashConversation(messages []model.AnthropicMessage) string { - var parts []string - for _, msg := range messages { - parts = append(parts, c.hashMessage(msg)) - } - - conversationData := strings.Join(parts, "|") - hash := sha256.Sum256([]byte(conversationData)) - return fmt.Sprintf("%x", hash[:16]) // Use first 16 bytes for conversation hash -} - -// normalizeMessageContent converts message content to a normalized string -func (c *ConversationDiffAnalyzer) normalizeMessageContent(content interface{}) string { - switch v := content.(type) { - case string: - return strings.TrimSpace(v) - case []interface{}: - var parts []string - for _, item := range v { - if block, ok := item.(map[string]interface{}); ok { - if text, hasText := block["text"].(string); hasText { - parts = append(parts, strings.TrimSpace(text)) - } else if blockType, hasType := block["type"].(string); hasType { - // Handle different content types (tool_use, etc.) - switch blockType { - case "tool_use": - if name, hasName := block["name"].(string); hasName { - parts = append(parts, fmt.Sprintf("TOOL:%s", name)) - } - case "tool_result": - parts = append(parts, "TOOL_RESULT") - default: - parts = append(parts, fmt.Sprintf("CONTENT:%s", blockType)) - } - } - } - } - return strings.Join(parts, " ") - default: - // Convert to JSON and back for normalization - jsonBytes, _ := json.Marshal(content) - return string(jsonBytes) - } -} - -// getMessagePreview creates a short preview of a message -func (c *ConversationDiffAnalyzer) getMessagePreview(msg model.AnthropicMessage) string { - content := c.normalizeMessageContent(msg.Content) - if len(content) > 100 { - return content[:100] + "..." - } - return content -} - -// max returns the maximum of two integers -func max(a, b int) int { - if a > b { - return a - } - return b -} diff --git a/proxy/internal/middleware/auth.go b/proxy/internal/middleware/auth.go index 95ab9d2..da0dfdd 100644 --- a/proxy/internal/middleware/auth.go +++ b/proxy/internal/middleware/auth.go @@ -9,10 +9,16 @@ import ( "github.com/seifghazi/claude-code-monitor/internal/config" ) +func writeJSON(w http.ResponseWriter, status int, v interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(v) +} + func Auth(cfg config.AuthConfig) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodOptions || r.URL.Path == "/health" { + if r.Method == http.MethodOptions || isPublicBypassPath(r.URL.Path) { next.ServeHTTP(w, r) return } @@ -33,15 +39,22 @@ func Auth(cfg config.AuthConfig) func(http.Handler) http.Handler { } w.Header().Set("WWW-Authenticate", `Bearer realm="claude-code-proxy"`) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - _ = json.NewEncoder(w).Encode(map[string]string{ + writeJSON(w, http.StatusUnauthorized, map[string]string{ "error": "unauthorized", }) }) } } +func isPublicBypassPath(path string) bool { + switch path { + case "/health", "/livez", "/openapi.json", "/openapi.yaml": + return true + default: + return false + } +} + func extractAuthToken(r *http.Request, cfg config.AuthConfig) (string, bool) { authHeader := strings.TrimSpace(r.Header.Get("Authorization")) if authHeader != "" { diff --git a/proxy/internal/middleware/auth_test.go b/proxy/internal/middleware/auth_test.go index fe25558..1c16330 100644 --- a/proxy/internal/middleware/auth_test.go +++ b/proxy/internal/middleware/auth_test.go @@ -102,7 +102,7 @@ func TestAuthAcceptsBearerAndAPIKey(t *testing.T) { } } -func TestAuthSkipsHealthAndOptions(t *testing.T) { +func TestAuthSkipsPublicDiscoveryRoutesAndOptions(t *testing.T) { handler := Auth(config.AuthConfig{ Enabled: true, Token: "secret", @@ -110,15 +110,18 @@ func TestAuthSkipsHealthAndOptions(t *testing.T) { w.WriteHeader(http.StatusOK) })) - req := httptest.NewRequest(http.MethodGet, "http://example.local/health", nil) - rr := httptest.NewRecorder() - handler.ServeHTTP(rr, req) - if rr.Code != http.StatusOK { - t.Fatalf("expected health request to bypass auth, got %d", rr.Code) + publicPaths := []string{"/health", "/openapi.json", "/openapi.yaml"} + for _, path := range publicPaths { + req := httptest.NewRequest(http.MethodGet, "http://example.local"+path, nil) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("expected %s request to bypass auth, got %d", path, rr.Code) + } } - req = httptest.NewRequest(http.MethodOptions, "http://example.local/v1/messages", nil) - rr = httptest.NewRecorder() + req := httptest.NewRequest(http.MethodOptions, "http://example.local/v1/messages", nil) + rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected OPTIONS request to bypass auth, got %d", rr.Code) diff --git a/proxy/internal/middleware/dashboard_auth.go b/proxy/internal/middleware/dashboard_auth.go new file mode 100644 index 0000000..67d037c --- /dev/null +++ b/proxy/internal/middleware/dashboard_auth.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "crypto/subtle" + "net/http" +) + +// DashboardAuth returns middleware that protects dashboard/data routes with +// HTTP Basic Auth. If password is empty, the middleware is a no-op (disabled). +// The username is always "admin". +func DashboardAuth(password string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if password == "" { + next.ServeHTTP(w, r) + return + } + + user, pass, ok := r.BasicAuth() + if !ok || + subtle.ConstantTimeCompare([]byte(user), []byte("admin")) != 1 || + subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 { + w.Header().Set("WWW-Authenticate", `Basic realm="Claude Code Proxy"`) + writeJSON(w, http.StatusUnauthorized, map[string]string{ + "error": "unauthorized", + }) + return + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/proxy/internal/middleware/dashboard_auth_protocol_test.go b/proxy/internal/middleware/dashboard_auth_protocol_test.go new file mode 100644 index 0000000..65aa3cc --- /dev/null +++ b/proxy/internal/middleware/dashboard_auth_protocol_test.go @@ -0,0 +1,41 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestDashboardAuthSetsWWWAuthenticateHeader(t *testing.T) { + handler := DashboardAuth("secret")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/dashboard", nil) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + if got := rr.Header().Get("WWW-Authenticate"); got != `Basic realm="Claude Code Proxy"` { + t.Fatalf("expected basic auth challenge header, got %q", got) + } +} + +func TestDashboardAuthRejectsWrongUsername(t *testing.T) { + called := false + handler := DashboardAuth("secret")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/dashboard", nil) + req.SetBasicAuth("not-admin", "secret") + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + if called { + t.Fatal("expected handler not to be called with wrong username") + } + if rr.Code != http.StatusUnauthorized { + t.Fatalf("expected 401, got %d", rr.Code) + } +} diff --git a/proxy/internal/middleware/dashboard_auth_test.go b/proxy/internal/middleware/dashboard_auth_test.go new file mode 100644 index 0000000..c81f502 --- /dev/null +++ b/proxy/internal/middleware/dashboard_auth_test.go @@ -0,0 +1,105 @@ +package middleware + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestDashboardAuthDisabledWhenEmpty(t *testing.T) { + called := false + handler := DashboardAuth("")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/dashboard", nil) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + if !called { + t.Fatal("expected handler to be called when password is empty") + } + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } +} + +func TestDashboardAuthRejectsNoCredentials(t *testing.T) { + handler := DashboardAuth("secret")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/dashboard", nil) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusUnauthorized { + t.Fatalf("expected 401, got %d", rr.Code) + } + + // Verify JSON response body + var body map[string]string + if err := json.NewDecoder(rr.Body).Decode(&body); err != nil { + t.Fatalf("expected JSON response, got error: %v", err) + } + if body["error"] != "unauthorized" { + t.Fatalf("expected error=unauthorized, got %q", body["error"]) + } +} + +func TestDashboardAuthRejectsWrongPassword(t *testing.T) { + handler := DashboardAuth("secret")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/dashboard", nil) + req.SetBasicAuth("admin", "wrong") + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusUnauthorized { + t.Fatalf("expected 401, got %d", rr.Code) + } +} + +func TestDashboardAuthAcceptsValidCredentials(t *testing.T) { + called := false + handler := DashboardAuth("secret")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/dashboard", nil) + req.SetBasicAuth("admin", "secret") + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + if !called { + t.Fatal("expected handler to be called with valid credentials") + } + if rr.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rr.Code) + } +} + +func TestWriteJSONSetsContentType(t *testing.T) { + rr := httptest.NewRecorder() + writeJSON(rr, http.StatusForbidden, map[string]string{"error": "forbidden"}) + + if ct := rr.Header().Get("Content-Type"); ct != "application/json" { + t.Fatalf("expected Content-Type application/json, got %q", ct) + } + if rr.Code != http.StatusForbidden { + t.Fatalf("expected status 403, got %d", rr.Code) + } + + var body map[string]string + if err := json.NewDecoder(rr.Body).Decode(&body); err != nil { + t.Fatalf("expected valid JSON, got error: %v", err) + } + if body["error"] != "forbidden" { + t.Fatalf("expected error=forbidden, got %q", body["error"]) + } +} diff --git a/proxy/internal/middleware/inflight.go b/proxy/internal/middleware/inflight.go new file mode 100644 index 0000000..b7c465f --- /dev/null +++ b/proxy/internal/middleware/inflight.go @@ -0,0 +1,22 @@ +package middleware + +import ( + "net/http" + + "github.com/seifghazi/claude-code-monitor/internal/runtime" +) + +// InFlight tracks the number of requests currently being served by the +// wrapped handler. Apply it to the /v1/* subrouter only — the gauge is +// meant to drive deploy-time draining decisions and shouldn't be polluted +// by fast dashboard or health-probe traffic. +// +// The decrement runs in a defer so a panicking handler can't strand the +// counter. +func InFlight(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + runtime.IncInFlight() + defer runtime.DecInFlight() + next.ServeHTTP(w, r) + }) +} diff --git a/proxy/internal/middleware/logging.go b/proxy/internal/middleware/logging.go index 433be43..60e2c9d 100644 --- a/proxy/internal/middleware/logging.go +++ b/proxy/internal/middleware/logging.go @@ -3,10 +3,12 @@ package middleware import ( "bytes" "context" + "encoding/json" "fmt" "io" "log" "net/http" + "strings" "time" "github.com/seifghazi/claude-code-monitor/internal/model" @@ -40,17 +42,174 @@ func Logging(next http.Handler) http.Handler { duration := time.Since(start) statusColor := getStatusColor(wrapped.statusCode) - log.Printf("%s %s %s%d%s %s (%s)", - r.Method, - r.URL.Path, - statusColor, - wrapped.statusCode, - colorReset, - http.StatusText(wrapped.statusCode), - formatDuration(duration)) + // Build a richer log line for proxy requests + if isProxyRequest(r.URL.Path) { + details := buildProxyLogDetails(r, bodyBytes, wrapped, duration) + log.Printf("%s%s%s %s", + statusColor, details, colorReset, + colorDim+formatDuration(duration)+colorReset) + } else { + log.Printf("%s %s %s%d%s (%s)", + r.Method, + r.URL.Path, + statusColor, + wrapped.statusCode, + colorReset, + formatDuration(duration)) + } }) } +// isProxyRequest returns true for /v1/* API proxy paths +func isProxyRequest(path string) bool { + return strings.HasPrefix(path, "/v1/") +} + +// buildProxyLogDetails creates a rich single-line log for proxy requests +func buildProxyLogDetails(r *http.Request, bodyBytes []byte, w *responseWriter, duration time.Duration) string { + var parts []string + + // Status + parts = append(parts, fmt.Sprintf("%d", w.statusCode)) + + // Method + path + parts = append(parts, fmt.Sprintf("%s %s", r.Method, r.URL.Path)) + + // Extract model and stream flag from request body + if len(bodyBytes) > 0 { + var body struct { + Model string `json:"model"` + Stream bool `json:"stream"` + } + if err := json.Unmarshal(bodyBytes, &body); err == nil { + if body.Model != "" { + // Shorten model name for readability + modelShort := shortenModel(body.Model) + parts = append(parts, colorCyan+modelShort+colorReset) + } + if body.Stream { + parts = append(parts, "stream") + } + } + } + + // Client info — use X-Forwarded-For if behind proxy, else RemoteAddr + clientIP := r.Header.Get("X-Forwarded-For") + if clientIP == "" { + clientIP = r.Header.Get("X-Real-Ip") + } + if clientIP == "" { + clientIP = r.RemoteAddr + } + // Strip port from IP + if host, _, err := splitHostPort(clientIP); err == nil { + clientIP = host + } + + // User-Agent — extract just the tool/client name + ua := r.Header.Get("User-Agent") + if clientName := extractClientName(ua); clientName != "" { + parts = append(parts, colorDim+clientName+colorReset) + } + + if clientIP != "" { + parts = append(parts, colorDim+clientIP+colorReset) + } + + return strings.Join(parts, " ") +} + +// shortenModel turns "claude-sonnet-4-20250514" into "sonnet-4" +func shortenModel(model string) string { + lower := strings.ToLower(model) + for _, family := range []string{"opus", "sonnet", "haiku"} { + if strings.Contains(lower, family) { + // Find version number after family name + idx := strings.Index(lower, family) + rest := lower[idx+len(family):] + // Extract version like "-4" or "-4-20250514" + rest = strings.TrimLeft(rest, "-") + if dashIdx := strings.Index(rest, "-"); dashIdx > 0 { + // Keep just the version number (e.g. "4" from "4-20250514") + return family + "-" + rest[:dashIdx] + } + if rest != "" { + return family + "-" + rest + } + return family + } + } + // For non-Claude models, take first two segments + segs := strings.SplitN(model, "-", 3) + if len(segs) >= 2 { + return segs[0] + "-" + segs[1] + } + return model +} + +// extractClientName pulls a recognizable client name from User-Agent +func extractClientName(ua string) string { + if ua == "" { + return "" + } + lower := strings.ToLower(ua) + switch { + case strings.Contains(lower, "claude-code"): + return "claude-code" + case strings.Contains(lower, "cursor"): + return "cursor" + case strings.Contains(lower, "continue"): + return "continue" + case strings.Contains(lower, "anthropic-sdk"): + return "sdk" + case strings.Contains(lower, "python"): + return "python" + case strings.Contains(lower, "node"): + return "node" + case strings.Contains(lower, "curl"): + return "curl" + default: + // Take first token + if spaceIdx := strings.IndexByte(ua, ' '); spaceIdx > 0 { + first := ua[:spaceIdx] + if len(first) > 20 { + return first[:20] + } + return first + } + if len(ua) > 20 { + return ua[:20] + } + return ua + } +} + +// splitHostPort is a simple wrapper that handles IPs without ports +func splitHostPort(addr string) (string, string, error) { + // If there's no colon or it's an IPv6 without port, just return the addr + if !strings.Contains(addr, ":") { + return addr, "", nil + } + // Handle comma-separated X-Forwarded-For + if commaIdx := strings.IndexByte(addr, ','); commaIdx > 0 { + addr = strings.TrimSpace(addr[:commaIdx]) + } + host, port, err := splitAddr(addr) + return host, port, err +} + +func splitAddr(addr string) (string, string, error) { + if strings.Count(addr, ":") > 1 { + // IPv6 — may or may not have brackets + return addr, "", nil + } + idx := strings.LastIndexByte(addr, ':') + if idx < 0 { + return addr, "", nil + } + return addr[:idx], addr[idx+1:], nil +} + type responseWriter struct { http.ResponseWriter statusCode int @@ -61,6 +220,16 @@ func (rw *responseWriter) WriteHeader(code int) { rw.ResponseWriter.WriteHeader(code) } +// Flush propagates to the underlying http.Flusher. Without this, embedding +// http.ResponseWriter (an interface) silently drops Flush(), so SSE writes +// buffer in net/http until the body closes — breaking token-by-token +// streaming UX. +func (rw *responseWriter) Flush() { + if f, ok := rw.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + // ANSI color codes const ( colorReset = "\033[0m" @@ -69,6 +238,7 @@ const ( colorRed = "\033[31m" colorBlue = "\033[34m" colorCyan = "\033[36m" + colorDim = "\033[2m" ) func getStatusColor(status int) string { diff --git a/proxy/internal/model/models.go b/proxy/internal/model/models.go index 1e5f5ef..253002a 100644 --- a/proxy/internal/model/models.go +++ b/proxy/internal/model/models.go @@ -9,6 +9,22 @@ type ContextKey string const BodyBytesKey ContextKey = "bodyBytes" +// ProxySettings holds dynamic proxy configuration (persisted in DB) +type ProxySettings struct { + RequestHeaderRules []HeaderRule `json:"requestHeaderRules"` + ResponseHeaderRules []HeaderRule `json:"responseHeaderRules"` +} + +// HeaderRule defines an action to take on a specific header. +// Actions: "block" (remove), "set" (override value), "replace" (find & replace in value) +type HeaderRule struct { + Header string `json:"header"` // Header name (case-insensitive match) + Action string `json:"action"` // "block", "set", "replace" + Value string `json:"value,omitempty"` // For "set": the new value. For "replace": the replacement string. + Find string `json:"find,omitempty"` // For "replace": the string to find in the header value + Enabled bool `json:"enabled"` // Toggle without deleting +} + type PromptGrade struct { Score int `json:"score"` MaxScore int `json:"maxScore"` @@ -25,19 +41,22 @@ type CriteriaScore struct { } type RequestLog struct { - RequestID string `json:"requestId"` - Timestamp string `json:"timestamp"` - Method string `json:"method"` - Endpoint string `json:"endpoint"` - Headers map[string][]string `json:"headers"` - Body interface{} `json:"body"` - Model string `json:"model,omitempty"` - OriginalModel string `json:"originalModel,omitempty"` - RoutedModel string `json:"routedModel,omitempty"` - UserAgent string `json:"userAgent"` - ContentType string `json:"contentType"` - PromptGrade *PromptGrade `json:"promptGrade,omitempty"` - Response *ResponseLog `json:"response,omitempty"` + RequestID string `json:"requestId"` + Timestamp string `json:"timestamp"` + Method string `json:"method"` + Endpoint string `json:"endpoint"` + Headers map[string][]string `json:"headers"` + Body interface{} `json:"body"` + Model string `json:"model,omitempty"` + OriginalModel string `json:"originalModel,omitempty"` + RoutedModel string `json:"routedModel,omitempty"` + UserAgent string `json:"userAgent"` + ContentType string `json:"contentType"` + PromptGrade *PromptGrade `json:"promptGrade,omitempty"` + Response *ResponseLog `json:"response,omitempty"` + ConversationHash string `json:"conversationHash,omitempty"` + MessageCount int `json:"messageCount,omitempty"` + OrganizationID string `json:"organizationId,omitempty"` } type ResponseLog struct { @@ -48,8 +67,42 @@ type ResponseLog struct { StreamError string `json:"streamError,omitempty"` ResponseTime int64 `json:"responseTime"` StreamingChunks []string `json:"streamingChunks,omitempty"` + ChunkTimings []ChunkTiming `json:"chunkTimings,omitempty"` IsStreaming bool `json:"isStreaming"` CompletedAt string `json:"completedAt"` + RateLimit *RateLimitInfo `json:"rateLimit,omitempty"` +} + +// ChunkTiming records when each SSE chunk arrived during streaming +type ChunkTiming struct { + Index int `json:"index"` + Timestamp string `json:"timestamp"` + ByteSize int `json:"byteSize"` + ElapsedMs int64 `json:"elapsedMs"` +} + +// RateLimitInfo captures rate limit / quota data from upstream response headers +type RateLimitInfo struct { + // Organization + OrganizationID string `json:"organizationId,omitempty"` + + // Legacy per-resource rate limits + RequestsLimit int `json:"requestsLimit,omitempty"` + RequestsRemaining int `json:"requestsRemaining,omitempty"` + RequestsReset string `json:"requestsReset,omitempty"` + TokensLimit int `json:"tokensLimit,omitempty"` + TokensRemaining int `json:"tokensRemaining,omitempty"` + TokensReset string `json:"tokensReset,omitempty"` + + // Unified quota system (Anthropic's current model) + UnifiedStatus string `json:"unifiedStatus,omitempty"` + UnifiedUtilization5h float64 `json:"unifiedUtilization5h,omitempty"` + UnifiedReset5h string `json:"unifiedReset5h,omitempty"` + UnifiedUtilization7d float64 `json:"unifiedUtilization7d,omitempty"` + UnifiedReset7d string `json:"unifiedReset7d,omitempty"` + UnifiedFallbackPercentage float64 `json:"unifiedFallbackPercentage,omitempty"` + UnifiedOverageStatus string `json:"unifiedOverageStatus,omitempty"` + UnifiedRepresentativeClaim string `json:"unifiedRepresentativeClaim,omitempty"` } type ChatMessage struct { @@ -132,7 +185,7 @@ type Tool struct { } type InputSchema struct { - Type string `json:"type"` + Type interface{} `json:"type"` Properties map[string]interface{} `json:"properties"` Required []string `json:"required,omitempty"` } @@ -176,6 +229,85 @@ type ErrorResponse struct { Details string `json:"details,omitempty"` } +// UsageStats represents aggregated token usage statistics +type UsageStats struct { + TotalRequests int `json:"total_requests"` + TotalInputTokens int64 `json:"total_input_tokens"` + TotalOutputTokens int64 `json:"total_output_tokens"` + TotalCacheTokens int64 `json:"total_cache_tokens"` + RequestsByModel map[string]ModelStats `json:"requests_by_model"` + StartDate string `json:"start_date,omitempty"` + EndDate string `json:"end_date,omitempty"` +} + +// ModelStats represents per-model usage statistics +type ModelStats struct { + RequestCount int `json:"request_count"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + CacheTokens int64 `json:"cache_tokens"` +} + +// RequestSummary is a lightweight version of RequestLog for fast list views +type RequestSummary struct { + RequestID string `json:"requestId"` + Timestamp string `json:"timestamp"` + Method string `json:"method"` + Endpoint string `json:"endpoint"` + Model string `json:"model,omitempty"` + OriginalModel string `json:"originalModel,omitempty"` + RoutedModel string `json:"routedModel,omitempty"` + StatusCode int `json:"statusCode,omitempty"` + ResponseTime int64 `json:"responseTime,omitempty"` + Usage *AnthropicUsage `json:"usage,omitempty"` + ConversationHash string `json:"conversationHash,omitempty"` + MessageCount int `json:"messageCount,omitempty"` + StopReason string `json:"stopReason,omitempty"` +} + +// Dashboard stats structures +type DashboardStats struct { + DailyStats []DailyTokens `json:"dailyStats"` +} + +type HourlyStatsResponse struct { + HourlyStats []HourlyTokens `json:"hourlyStats"` + TodayTokens int64 `json:"todayTokens"` + TodayRequests int `json:"todayRequests"` + AvgResponseTime int64 `json:"avgResponseTime"` +} + +type ModelStatsResponse struct { + ModelStats []ModelTokens `json:"modelStats"` +} + +type DailyTokens struct { + Date string `json:"date"` + Tokens int64 `json:"tokens"` + Requests int `json:"requests"` + Models map[string]DailyModelStat `json:"models,omitempty"` +} + +type HourlyTokens struct { + Hour int `json:"hour"` + Label string `json:"label,omitempty"` + Tokens int64 `json:"tokens"` + Requests int `json:"requests"` + Models map[string]DailyModelStat `json:"models,omitempty"` +} + +// DailyModelStat represents per-model stats for dashboard aggregation +type DailyModelStat struct { + Tokens int64 `json:"tokens"` + Requests int `json:"requests"` +} + +type ModelTokens struct { + Model string `json:"model"` + Tokens int64 `json:"tokens"` + Requests int `json:"requests"` +} + type StreamingEvent struct { Type string `json:"type"` Index *int `json:"index,omitempty"` diff --git a/proxy/internal/provider/anthropic.go b/proxy/internal/provider/anthropic.go index 9c39389..f09ec3f 100644 --- a/proxy/internal/provider/anthropic.go +++ b/proxy/internal/provider/anthropic.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "net" "net/http" "net/url" "path" @@ -20,9 +21,29 @@ type AnthropicProvider struct { } func NewAnthropicProvider(cfg *config.AnthropicProviderConfig) Provider { + respHeaderTimeout := cfg.ResponseHeaderTimeout + if respHeaderTimeout <= 0 { + respHeaderTimeout = 300 * time.Second + } return &AnthropicProvider{ client: &http.Client{ - Timeout: 300 * time.Second, // 5 minutes timeout + // No Client.Timeout: a global timeout would cancel long streaming + // responses mid-flight. Per-phase timeouts on the Transport plus the + // 30-min context in handlers.Messages bound the request instead. + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 30 * time.Second, + // Tunable via ANTHROPIC_RESPONSE_HEADER_TIMEOUT — opus + extended + // thinking on large contexts can take longer than the 300s default. + ResponseHeaderTimeout: respHeaderTimeout, + ExpectContinueTimeout: 1 * time.Second, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + }, }, config: cfg, } diff --git a/proxy/internal/provider/openai.go b/proxy/internal/provider/openai.go index 7afcc16..c2f18bb 100644 --- a/proxy/internal/provider/openai.go +++ b/proxy/internal/provider/openai.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" "net/url" "strings" @@ -25,7 +26,26 @@ type OpenAIProvider struct { func NewOpenAIProvider(cfg *config.OpenAIProviderConfig) Provider { return &OpenAIProvider{ client: &http.Client{ - Timeout: 300 * time.Second, // 5 minutes timeout + // No timeout set here - we rely on context cancellation for timeouts. + // Setting Timeout here would apply to the entire request+response cycle, + // which causes "context canceled" errors for long-running streaming + // requests with large "thinking" content blocks. + // The server's WriteTimeout handles individual write operations, + // and the context passed to ForwardRequest controls the overall timeout. + Transport: &http.Transport{ + // Connection timeouts + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 30 * time.Second, + ResponseHeaderTimeout: 300 * time.Second, // Time to wait for response headers (high for 1M context) + ExpectContinueTimeout: 1 * time.Second, + // Connection pooling + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + }, }, config: cfg, } @@ -183,182 +203,260 @@ func (p *OpenAIProvider) ForwardRequest(ctx context.Context, originalReq *http.R return resp, nil } +// extractSystemMessages combines all system messages into a single string for OpenAI. +func extractSystemMessages(system []model.AnthropicSystemMessage) string { + if len(system) == 0 { + return "" + } + var parts []string + for _, sysMsg := range system { + parts = append(parts, sysMsg.Text) + } + return strings.Join(parts, "\n\n") +} + +// convertToolResultContent converts the various formats of tool result content to a string. +func convertToolResultContent(content interface{}) string { + if content == nil { + return "" + } + + switch v := content.(type) { + case string: + return v + case []interface{}: + var result string + for _, c := range v { + if contentMap, ok := c.(map[string]interface{}); ok { + if contentMap["type"] == "text" { + if text, ok := contentMap["text"].(string); ok { + result += text + "\n" + } + } else if text, hasText := contentMap["text"]; hasText { + result += fmt.Sprintf("%v\n", text) + } else { + if jsonBytes, err := json.Marshal(contentMap); err == nil { + result += string(jsonBytes) + "\n" + } else { + result += fmt.Sprintf("%v\n", contentMap) + } + } + } + } + return result + case map[string]interface{}: + if v["type"] == "text" { + if text, ok := v["text"].(string); ok { + return text + } + } + if jsonBytes, err := json.Marshal(v); err == nil { + return string(jsonBytes) + } + return fmt.Sprintf("%v", v) + default: + if jsonBytes, err := json.Marshal(content); err == nil { + return string(jsonBytes) + } + return fmt.Sprintf("%v", content) + } +} + +// convertMessageContent converts an Anthropic message's content to a plain text string for OpenAI. +// It handles both content arrays (with possible tool results) and simple string content. +func convertMessageContent(msg model.AnthropicMessage) string { + contentArray, ok := msg.Content.([]interface{}) + if !ok { + // Handle simple string content via GetContentBlocks + contentBlocks := msg.GetContentBlocks() + var parts []string + for _, block := range contentBlocks { + if block.Type == "text" { + parts = append(parts, block.Text) + } + } + content := strings.Join(parts, "\n") + if content == "" { + content = "..." + } + return content + } + + // Check if this message contains tool results + hasToolResults := false + for _, item := range contentArray { + if block, ok := item.(map[string]interface{}); ok { + if blockType, _ := block["type"].(string); blockType == "tool_result" { + hasToolResults = true + break + } + } + } + + if hasToolResults { + return convertContentArrayWithToolResults(contentArray) + } + return convertRegularContentArray(contentArray) +} + +// convertContentArrayWithToolResults handles content arrays that contain tool_result blocks. +func convertContentArrayWithToolResults(contentArray []interface{}) string { + textContent := "" + + for _, item := range contentArray { + block, ok := item.(map[string]interface{}) + if !ok { + continue + } + blockType, _ := block["type"].(string) + switch blockType { + case "text": + if text, ok := block["text"].(string); ok { + textContent += text + "\n" + } + case "tool_result": + toolID := "" + if id, ok := block["tool_use_id"].(string); ok { + toolID = id + } + resultContent := convertToolResultContent(block["content"]) + textContent += fmt.Sprintf("Tool result for %s:\n%s\n", toolID, resultContent) + } + } + + if textContent == "" { + return "..." + } + return strings.TrimSpace(textContent) +} + +// convertRegularContentArray handles content arrays with only text blocks. +func convertRegularContentArray(contentArray []interface{}) string { + var parts []string + for _, item := range contentArray { + if block, ok := item.(map[string]interface{}); ok { + if blockType, _ := block["type"].(string); blockType == "text" { + if text, ok := block["text"].(string); ok { + parts = append(parts, text) + } + } + } + } + content := strings.Join(parts, "\n") + if content == "" { + content = "..." + } + return content +} + +// convertToolsToOpenAI converts Anthropic tool definitions to OpenAI format. +func convertToolsToOpenAI(tools []model.Tool) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(tools)) + for _, tool := range tools { + if tool.Name == "" { + continue + } + + parameters := make(map[string]interface{}) + if tool.InputSchema.Type != nil { + parameters["type"] = tool.InputSchema.Type + } else { + parameters["type"] = "object" + } + + if tool.InputSchema.Properties != nil { + fixedProperties := make(map[string]interface{}) + for propName, propValue := range tool.InputSchema.Properties { + if prop, ok := propValue.(map[string]interface{}); ok { + if propType, hasType := prop["type"]; hasType && propType == "array" { + if _, hasItems := prop["items"]; !hasItems { + prop["items"] = map[string]interface{}{"type": "string"} + } + } + fixedProperties[propName] = prop + } else { + fixedProperties[propName] = propValue + } + } + parameters["properties"] = fixedProperties + } else { + parameters["properties"] = make(map[string]interface{}) + } + + if len(tool.InputSchema.Required) > 0 { + parameters["required"] = tool.InputSchema.Required + } + + functionDef := map[string]interface{}{ + "name": tool.Name, + "parameters": parameters, + } + if tool.Description != "" { + functionDef["description"] = tool.Description + } + + result = append(result, map[string]interface{}{ + "type": "function", + "function": functionDef, + }) + } + return result +} + +// convertToolChoice converts Anthropic tool_choice to OpenAI format. +func convertToolChoice(toolChoice interface{}) interface{} { + if toolChoice == nil { + return nil + } + toolChoiceMap, ok := toolChoice.(map[string]interface{}) + if !ok { + return nil + } + switch toolChoiceMap["type"] { + case "auto": + return "auto" + case "any": + return "required" + case "tool": + if name, ok := toolChoiceMap["name"].(string); ok { + return map[string]interface{}{ + "type": "function", + "function": map[string]interface{}{ + "name": name, + }, + } + } + return "auto" + default: + return "auto" + } +} + func convertAnthropicToOpenAI(req *model.AnthropicRequest) map[string]interface{} { messages := []map[string]interface{}{} - // Combine all system messages into a single system message for OpenAI - if len(req.System) > 0 { - systemContent := "" - for i, sysMsg := range req.System { - if i > 0 { - systemContent += "\n\n" - } - systemContent += sysMsg.Text - } + // Add system message if present + if systemContent := extractSystemMessages(req.System); systemContent != "" { messages = append(messages, map[string]interface{}{ "role": "system", "content": systemContent, }) } - // Add conversation messages + // Convert conversation messages for _, msg := range req.Messages { - // Handle messages with raw content that may contain tool results - if contentArray, ok := msg.Content.([]interface{}); ok { - // Check if this message contains tool results - hasToolResults := false - for _, item := range contentArray { - if block, ok := item.(map[string]interface{}); ok { - if blockType, hasType := block["type"].(string); hasType && blockType == "tool_result" { - hasToolResults = true - break - } - } - } - - if hasToolResults { - textContent := "" - - for _, item := range contentArray { - if block, ok := item.(map[string]interface{}); ok { - if blockType, hasType := block["type"].(string); hasType { - if blockType == "text" { - if text, hasText := block["text"].(string); hasText { - textContent += text + "\n" - } - } else if blockType == "tool_result" { - // Extract tool ID - toolID := "" - if id, hasID := block["tool_use_id"].(string); hasID { - toolID = id - } - - // Handle different formats of tool result content - resultContent := "" - if content, hasContent := block["content"]; hasContent { - if contentStr, ok := content.(string); ok { - resultContent = contentStr - } else if contentList, ok := content.([]interface{}); ok { - // If content is a list of blocks, extract text from each - for _, c := range contentList { - if contentMap, ok := c.(map[string]interface{}); ok { - if contentMap["type"] == "text" { - if text, ok := contentMap["text"].(string); ok { - resultContent += text + "\n" - } - } else if text, hasText := contentMap["text"]; hasText { - // Handle any dict by trying to extract text - resultContent += fmt.Sprintf("%v\n", text) - } else { - // Try to JSON serialize - if jsonBytes, err := json.Marshal(contentMap); err == nil { - resultContent += string(jsonBytes) + "\n" - } else { - resultContent += fmt.Sprintf("%v\n", contentMap) - } - } - } - } - } else if contentDict, ok := content.(map[string]interface{}); ok { - // Handle dictionary content - if contentDict["type"] == "text" { - if text, ok := contentDict["text"].(string); ok { - resultContent = text - } - } else { - // Try to JSON serialize - if jsonBytes, err := json.Marshal(contentDict); err == nil { - resultContent = string(jsonBytes) - } else { - resultContent = fmt.Sprintf("%v", contentDict) - } - } - } else { - // Handle any other type by converting to string - if jsonBytes, err := json.Marshal(content); err == nil { - resultContent = string(jsonBytes) - } else { - resultContent = fmt.Sprintf("%v", content) - } - } - } - - // In OpenAI format, tool results come from the user (matching Python behavior) - textContent += fmt.Sprintf("Tool result for %s:\n%s\n", toolID, resultContent) - } - } - } - } - - // Add as a single user message with all the content - if textContent == "" { - textContent = "..." - } - messages = append(messages, map[string]interface{}{ - "role": msg.Role, - "content": strings.TrimSpace(textContent), - }) - } else { - // Handle regular messages with content blocks - content := "" - - for _, item := range contentArray { - if block, ok := item.(map[string]interface{}); ok { - if blockType, hasType := block["type"].(string); hasType && blockType == "text" { - if text, hasText := block["text"].(string); hasText { - if content != "" { - content += "\n" - } - content += text - } - } - } - } - - // Ensure content is never empty - if content == "" { - content = "..." - } - - messages = append(messages, map[string]interface{}{ - "role": msg.Role, - "content": content, - }) - } - } else { - // Handle simple string content - contentBlocks := msg.GetContentBlocks() - content := "" - - // Concatenate all text blocks - for _, block := range contentBlocks { - if block.Type == "text" { - if content != "" { - content += "\n" - } - content += block.Text - } - } - - // Ensure content is never empty - if content == "" { - content = "..." - } - - messages = append(messages, map[string]interface{}{ - "role": msg.Role, - "content": content, - }) - } + messages = append(messages, map[string]interface{}{ + "role": msg.Role, + "content": convertMessageContent(msg), + }) } + // Get model-specific max token limit - // Let the API handle validation for unknown models rather than using arbitrary caps maxTokensLimit := getModelMaxTokens(req.Model) if maxTokensLimit > 0 && req.MaxTokens > maxTokensLimit { req.MaxTokens = maxTokensLimit } - // All OpenAI models now use max_completion_tokens instead of deprecated max_tokens openAIReq := map[string]interface{}{ "model": req.Model, "messages": messages, @@ -366,110 +464,25 @@ func convertAnthropicToOpenAI(req *model.AnthropicRequest) map[string]interface{ "max_completion_tokens": req.MaxTokens, } - // If streaming is enabled, request usage data to be included in the final chunk if req.Stream { openAIReq["stream_options"] = map[string]interface{}{ "include_usage": true, } } - // Check if this is an o-series model (they don't support temperature) + // o-series models don't support temperature isOSeriesModel := strings.HasPrefix(req.Model, "o1") || strings.HasPrefix(req.Model, "o3") - - // Only include temperature for non-o-series models if !isOSeriesModel { openAIReq["temperature"] = req.Temperature } - // Convert Anthropic tools to OpenAI format + + // Convert tools and tool_choice if len(req.Tools) > 0 { - tools := make([]map[string]interface{}, 0, len(req.Tools)) - for _, tool := range req.Tools { - // Ensure tool has required fields - if tool.Name == "" { - // Skip tools with empty names - continue - } + openAIReq["tools"] = convertToolsToOpenAI(req.Tools) - // Build parameters with error checking - parameters := make(map[string]interface{}) - parameters["type"] = tool.InputSchema.Type - if parameters["type"] == "" { - parameters["type"] = "object" // Default to object type - } - - // Handle properties safely with array validation - if tool.InputSchema.Properties != nil { - // Fix array properties that are missing items field - fixedProperties := make(map[string]interface{}) - for propName, propValue := range tool.InputSchema.Properties { - if prop, ok := propValue.(map[string]interface{}); ok { - // Check if this is an array type missing items - if propType, hasType := prop["type"]; hasType && propType == "array" { - if _, hasItems := prop["items"]; !hasItems { - // Add default items definition for arrays - // Add default items for array properties missing them - prop["items"] = map[string]interface{}{"type": "string"} - } - } - fixedProperties[propName] = prop - } else { - // Keep non-map properties as-is - fixedProperties[propName] = propValue - } - } - parameters["properties"] = fixedProperties - } else { - parameters["properties"] = make(map[string]interface{}) - } - - // Handle required fields - if len(tool.InputSchema.Required) > 0 { - parameters["required"] = tool.InputSchema.Required - } - - // Build function definition - functionDef := map[string]interface{}{ - "name": tool.Name, - "parameters": parameters, - } - - // Add description if present - if tool.Description != "" { - functionDef["description"] = tool.Description - } - - openAITool := map[string]interface{}{ - "type": "function", - "function": functionDef, - } - tools = append(tools, openAITool) - } - openAIReq["tools"] = tools - - // Handle tool_choice if present if req.ToolChoice != nil { - // Convert Anthropic tool_choice to OpenAI format - if toolChoiceMap, ok := req.ToolChoice.(map[string]interface{}); ok { - choiceType := toolChoiceMap["type"] - switch choiceType { - case "auto": - openAIReq["tool_choice"] = "auto" - case "any": - openAIReq["tool_choice"] = "required" - case "tool": - // Specific tool choice - if name, hasName := toolChoiceMap["name"].(string); hasName { - openAIReq["tool_choice"] = map[string]interface{}{ - "type": "function", - "function": map[string]interface{}{ - "name": name, - }, - } - } - default: - // Default to auto if we can't determine - openAIReq["tool_choice"] = "auto" - } + if choice := convertToolChoice(req.ToolChoice); choice != nil { + openAIReq["tool_choice"] = choice } } } @@ -485,13 +498,6 @@ func getMapKeys(m map[string]interface{}) []string { return keys } -func min(a, b int) int { - if a < b { - return a - } - return b -} - // getModelMaxTokens returns the max output tokens for known models // Returns 0 for unknown models, letting the API handle validation func getModelMaxTokens(model string) int { diff --git a/proxy/internal/runtime/runtime.go b/proxy/internal/runtime/runtime.go new file mode 100644 index 0000000..e01838a --- /dev/null +++ b/proxy/internal/runtime/runtime.go @@ -0,0 +1,34 @@ +// Package runtime exposes process-level operational state shared between +// HTTP middleware, handlers, and the shutdown loop in main: a live in-flight +// request gauge for /livez, and a draining flag that flips on SIGTERM so +// /health goes non-ready before we wait for in-flight requests to drain. +// +// All state is package-level + atomic so callers don't need to plumb a struct +// through every middleware/handler. The gauge is decremented in a deferred +// call regardless of panics, so a misbehaving handler can't strand the count. +package runtime + +import "sync/atomic" + +var ( + inFlight atomic.Int64 + draining atomic.Bool +) + +// IncInFlight is called when a tracked request begins. Returns the new count. +func IncInFlight() int64 { return inFlight.Add(1) } + +// DecInFlight is called when a tracked request completes. Returns the new count. +func DecInFlight() int64 { return inFlight.Add(-1) } + +// InFlight returns the current number of tracked requests in progress. +func InFlight() int64 { return inFlight.Load() } + +// IsDraining reports whether the process is shutting down. Used by /health +// to advertise non-ready state so Traefik (or any LB doing health-based +// routing) stops sending new requests to this slot before drain begins. +func IsDraining() bool { return draining.Load() } + +// SetDraining flips the draining flag. Idempotent — safe to call from a +// signal handler. +func SetDraining(v bool) { draining.Store(v) } diff --git a/proxy/internal/service/anthropic.go b/proxy/internal/service/anthropic.go deleted file mode 100644 index ae32aca..0000000 --- a/proxy/internal/service/anthropic.go +++ /dev/null @@ -1,122 +0,0 @@ -package service - -import ( - "compress/gzip" - "context" - "fmt" - "io" - "net/http" - "net/url" - "path" - "strings" - "time" - - "github.com/seifghazi/claude-code-monitor/internal/config" -) - -type AnthropicService interface { - ForwardRequest(ctx context.Context, originalReq *http.Request) (*http.Response, error) -} - -type anthropicService struct { - client *http.Client - config *config.AnthropicConfig -} - -func NewAnthropicService(cfg *config.AnthropicConfig) AnthropicService { - return &anthropicService{ - client: &http.Client{ - Timeout: 300 * time.Second, // Increased timeout to 5 minutes - }, - config: cfg, - } -} - -func (s *anthropicService) ForwardRequest(ctx context.Context, originalReq *http.Request) (*http.Response, error) { - // Clone the request to avoid modifying the original - proxyReq := originalReq.Clone(ctx) - - // Parse the configured base URL - baseURL, err := url.Parse(s.config.BaseURL) - if err != nil { - return nil, fmt.Errorf("failed to parse base URL '%s': %w", s.config.BaseURL, err) - } - - if baseURL.Scheme == "" || baseURL.Host == "" { - return nil, fmt.Errorf("invalid base URL, scheme and host are required: %s", s.config.BaseURL) - } - - // Update the destination URL - proxyReq.URL.Scheme = baseURL.Scheme - proxyReq.URL.Host = baseURL.Host - proxyReq.URL.Path = path.Join(baseURL.Path, "/v1/messages") - - // Preserve query parameters from original request - proxyReq.URL.RawQuery = originalReq.URL.RawQuery - - // Clear fields that can't be set in client requests - proxyReq.RequestURI = "" // This is set by the server and must be cleared - proxyReq.Host = "" // Let Go set this from the URL - - // Forward the request with all original headers intact - resp, err := s.client.Do(proxyReq) - if err != nil { - return nil, fmt.Errorf("failed to send request: %w", err) - } - - // Handle gzip decompression - if strings.Contains(resp.Header.Get("Content-Encoding"), "gzip") { - decompressedResp, err := s.decompressGzipResponse(resp) - if err != nil { - resp.Body.Close() - return nil, fmt.Errorf("failed to decompress gzip response: %w", err) - } - return decompressedResp, nil - } - - return resp, nil -} - -func (s *anthropicService) decompressGzipResponse(resp *http.Response) (*http.Response, error) { - // Create a gzip reader - gzipReader, err := gzip.NewReader(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to create gzip reader: %w", err) - } - - // Read the decompressed data - decompressedData, err := io.ReadAll(gzipReader) - if err != nil { - gzipReader.Close() - return nil, fmt.Errorf("failed to read decompressed data: %w", err) - } - - // Close the gzip reader and original body - gzipReader.Close() - resp.Body.Close() - - // Create a new response with decompressed body - newResp := &http.Response{ - Status: resp.Status, - StatusCode: resp.StatusCode, - Proto: resp.Proto, - ProtoMajor: resp.ProtoMajor, - ProtoMinor: resp.ProtoMinor, - Header: resp.Header.Clone(), - ContentLength: int64(len(decompressedData)), - TransferEncoding: resp.TransferEncoding, - Close: resp.Close, - Uncompressed: true, - Trailer: resp.Trailer, - Request: resp.Request, - TLS: resp.TLS, - } - - // Remove Content-Encoding header since we've decompressed - newResp.Header.Del("Content-Encoding") - - // Set the decompressed body - newResp.Body = io.NopCloser(strings.NewReader(string(decompressedData))) - - return newResp, nil -} diff --git a/proxy/internal/service/conversation.go b/proxy/internal/service/conversation.go index ddf36e9..74799cd 100644 --- a/proxy/internal/service/conversation.go +++ b/proxy/internal/service/conversation.go @@ -48,6 +48,7 @@ type Conversation struct { SessionID string `json:"sessionId"` ProjectPath string `json:"projectPath"` ProjectName string `json:"projectName"` + Model string `json:"model,omitempty"` Messages []*ConversationMessage `json:"messages"` StartTime time.Time `json:"startTime"` EndTime time.Time `json:"endTime"` @@ -313,6 +314,7 @@ func (cs *conversationService) parseConversationFile(filePath, projectPath strin var messages []*ConversationMessage var parseErrors int lineNum := 0 + conversationModel := "" scanner := bufio.NewScanner(file) @@ -354,6 +356,15 @@ func (cs *conversationService) parseConversationFile(filePath, projectPath strin } messages = append(messages, &msg) + + // Claude conversation JSONL records the assistant model inside the nested message object. + // Track the latest model we see so the list view can filter by the active model tier. + var messageMeta struct { + Model string `json:"model"` + } + if err := json.Unmarshal(msg.Message, &messageMeta); err == nil && messageMeta.Model != "" { + conversationModel = messageMeta.Model + } } if err := scanner.Err(); err != nil { @@ -382,6 +393,7 @@ func (cs *conversationService) parseConversationFile(filePath, projectPath strin SessionID: sessionID, ProjectPath: projectPath, ProjectName: projectName, + Model: conversationModel, Messages: messages, StartTime: time.Time{}, EndTime: time.Time{}, @@ -425,6 +437,7 @@ func (cs *conversationService) parseConversationFile(filePath, projectPath strin SessionID: sessionID, ProjectPath: projectPath, ProjectName: projectName, + Model: conversationModel, Messages: messages, StartTime: startTime, EndTime: endTime, diff --git a/proxy/internal/service/conversation_test.go b/proxy/internal/service/conversation_test.go index 4c79cca..063e0b0 100644 --- a/proxy/internal/service/conversation_test.go +++ b/proxy/internal/service/conversation_test.go @@ -14,7 +14,10 @@ func TestConversationServiceAllowsNestedProjectPaths(t *testing.T) { } sessionPath := filepath.Join(projectDir, "session.jsonl") - if err := os.WriteFile(sessionPath, []byte(`{"timestamp":"2026-03-19T12:00:00Z","type":"user","message":"hello"}`+"\n"), 0o600); err != nil { + if err := os.WriteFile(sessionPath, []byte( + `{"timestamp":"2026-03-19T12:00:00Z","type":"user","message":"hello"}`+"\n"+ + `{"timestamp":"2026-03-19T12:00:01Z","type":"assistant","message":{"model":"claude-opus-4-6","role":"assistant","content":[{"type":"text","text":"hi"}]}}`+"\n", + ), 0o600); err != nil { t.Fatalf("WriteFile() error = %v", err) } @@ -33,8 +36,12 @@ func TestConversationServiceAllowsNestedProjectPaths(t *testing.T) { t.Fatalf("expected project path %q, got %q", "team/app", conversation.ProjectPath) } - if len(conversation.Messages) != 1 { - t.Fatalf("expected 1 message, got %d", len(conversation.Messages)) + if len(conversation.Messages) != 2 { + t.Fatalf("expected 2 messages, got %d", len(conversation.Messages)) + } + + if conversation.Model != "claude-opus-4-6" { + t.Fatalf("expected model %q, got %q", "claude-opus-4-6", conversation.Model) } conversations, err := svc.GetConversationsByProject("team/app") diff --git a/proxy/internal/service/storage.go b/proxy/internal/service/storage.go index da0b05e..703cf50 100644 --- a/proxy/internal/service/storage.go +++ b/proxy/internal/service/storage.go @@ -24,6 +24,22 @@ type StorageService interface { DeleteRequestsOlderThan(age time.Duration) (int, error) GetDatabaseStats() (map[string]interface{}, error) + // Usage statistics + GetUsageStats(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) + + // Dashboard statistics (fast aggregated queries) + GetRequestsSummary(modelFilter string) ([]*model.RequestSummary, error) + GetRequestsSummaryPaginated(modelFilter, startTime, endTime string, offset, limit int) ([]*model.RequestSummary, int, error) + GetStats(startDate, endDate, orgFilter string) (*model.DashboardStats, error) + GetHourlyStats(startTime, endTime string, bucketMinutes int, orgFilter string) (*model.HourlyStatsResponse, error) + GetModelStats(startTime, endTime, orgFilter string) (*model.ModelStatsResponse, error) + GetLatestRequestDate() (*time.Time, error) + GetDistinctOrganizations() ([]string, error) + + // Settings (dynamic proxy config) + GetSettings() (*model.ProxySettings, error) + SaveSettings(settings *model.ProxySettings) error + // Configuration GetConfig() *config.StorageConfig EnsureDirectoryExists() error diff --git a/proxy/internal/service/storage_aggregation_test.go b/proxy/internal/service/storage_aggregation_test.go new file mode 100644 index 0000000..14abcac --- /dev/null +++ b/proxy/internal/service/storage_aggregation_test.go @@ -0,0 +1,64 @@ +package service + +import ( + "testing" + + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +func TestAggregationHelpers(t *testing.T) { + t.Parallel() + + t.Run("addDailyTokens accumulates by date and model", func(t *testing.T) { + t.Parallel() + + dailyMap := make(map[string]*model.DailyTokens) + addDailyTokens(dailyMap, "2026-03-20", "claude", 10) + addDailyTokens(dailyMap, "2026-03-20", "claude", 5) + addDailyTokens(dailyMap, "2026-03-20", "gpt", 7) + + day := dailyMap["2026-03-20"] + if day == nil || day.Tokens != 22 || day.Requests != 3 { + t.Fatalf("unexpected daily aggregate: %#v", day) + } + if day.Models["claude"].Tokens != 15 || day.Models["claude"].Requests != 2 { + t.Fatalf("unexpected claude daily model aggregate: %#v", day.Models["claude"]) + } + if day.Models["gpt"].Tokens != 7 || day.Models["gpt"].Requests != 1 { + t.Fatalf("unexpected gpt daily model aggregate: %#v", day.Models["gpt"]) + } + }) + + t.Run("addHourlyTokens accumulates by bucket and model", func(t *testing.T) { + t.Parallel() + + bucketMap := make(map[string]*model.HourlyTokens) + addHourlyTokens(bucketMap, "09", "09:00", "claude", 4) + addHourlyTokens(bucketMap, "09", "09:00", "claude", 6) + addHourlyTokens(bucketMap, "09", "09:00", "gpt", 2) + + bucket := bucketMap["09"] + if bucket == nil || bucket.Tokens != 12 || bucket.Requests != 3 || bucket.Label != "09:00" { + t.Fatalf("unexpected hourly aggregate: %#v", bucket) + } + if bucket.Models["claude"].Tokens != 10 || bucket.Models["claude"].Requests != 2 { + t.Fatalf("unexpected claude hourly model aggregate: %#v", bucket.Models["claude"]) + } + if bucket.Models["gpt"].Tokens != 2 || bucket.Models["gpt"].Requests != 1 { + t.Fatalf("unexpected gpt hourly model aggregate: %#v", bucket.Models["gpt"]) + } + }) + + t.Run("addModelTokens accumulates by model", func(t *testing.T) { + t.Parallel() + + modelMap := make(map[string]*model.ModelTokens) + addModelTokens(modelMap, "claude", 8) + addModelTokens(modelMap, "claude", 12) + + got := modelMap["claude"] + if got == nil || got.Tokens != 20 || got.Requests != 2 { + t.Fatalf("unexpected model aggregate: %#v", got) + } + }) +} diff --git a/proxy/internal/service/storage_analytics.go b/proxy/internal/service/storage_analytics.go new file mode 100644 index 0000000..929e6c3 --- /dev/null +++ b/proxy/internal/service/storage_analytics.go @@ -0,0 +1,163 @@ +package service + +import ( + "database/sql" + "encoding/json" + + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +type responseBodySummary struct { + Usage *model.AnthropicUsage `json:"usage"` + StopReason string `json:"stop_reason"` +} + +func decodeStoredResponse(responseJSON sql.NullString) (*model.ResponseLog, bool) { + if !responseJSON.Valid || responseJSON.String == "" { + return nil, false + } + + var resp model.ResponseLog + if err := json.Unmarshal([]byte(responseJSON.String), &resp); err != nil { + return nil, false + } + + return &resp, true +} + +func decodeResponseBodySummary(body json.RawMessage) (*responseBodySummary, bool) { + if len(body) == 0 { + return nil, false + } + + var summary responseBodySummary + if err := json.Unmarshal(body, &summary); err != nil { + return nil, false + } + + return &summary, true +} + +func totalTokensFromUsage(usage *model.AnthropicUsage) int64 { + if usage == nil { + return 0 + } + + return int64( + usage.InputTokens + + usage.OutputTokens + + usage.CacheReadInputTokens + + usage.CacheCreationInputTokens) +} + +func totalTokensFromStoredResponse(responseJSON sql.NullString) int64 { + input, output, cache, ok := usageCountsFromStoredResponse(responseJSON) + if !ok { + return 0 + } + + return input + output + cache +} + +func responseTimeFromStoredResponse(responseJSON sql.NullString) int64 { + resp, ok := decodeStoredResponse(responseJSON) + if !ok { + return 0 + } + + return resp.ResponseTime +} + +func applyStoredResponseToSummary(summary *model.RequestSummary, responseJSON sql.NullString) { + resp, ok := decodeStoredResponse(responseJSON) + if !ok { + return + } + + summary.StatusCode = resp.StatusCode + summary.ResponseTime = resp.ResponseTime + + bodySummary, ok := decodeResponseBodySummary(resp.Body) + if !ok { + return + } + + summary.Usage = bodySummary.Usage + summary.StopReason = bodySummary.StopReason +} + +func usageCountsFromStoredResponse(responseJSON sql.NullString) (input, output, cache int64, ok bool) { + resp, ok := decodeStoredResponse(responseJSON) + if !ok { + return 0, 0, 0, false + } + + bodySummary, ok := decodeResponseBodySummary(resp.Body) + if !ok || bodySummary.Usage == nil { + return 0, 0, 0, false + } + + return int64(bodySummary.Usage.InputTokens), + int64(bodySummary.Usage.OutputTokens), + int64(bodySummary.Usage.CacheCreationInputTokens + bodySummary.Usage.CacheReadInputTokens), + true +} + +func addDailyTokens(dailyMap map[string]*model.DailyTokens, date, modelName string, tokens int64) { + if daily, ok := dailyMap[date]; ok { + daily.Tokens += tokens + daily.Requests++ + daily.Models = addDailyModelStat(daily.Models, modelName, tokens) + return + } + + dailyMap[date] = &model.DailyTokens{ + Date: date, + Tokens: tokens, + Requests: 1, + Models: addDailyModelStat(nil, modelName, tokens), + } +} + +func addHourlyTokens(bucketMap map[string]*model.HourlyTokens, bucketKey, bucketLabel, modelName string, tokens int64) { + if bucket, ok := bucketMap[bucketKey]; ok { + bucket.Tokens += tokens + bucket.Requests++ + bucket.Models = addDailyModelStat(bucket.Models, modelName, tokens) + return + } + + bucketMap[bucketKey] = &model.HourlyTokens{ + Hour: 0, + Label: bucketLabel, + Tokens: tokens, + Requests: 1, + Models: addDailyModelStat(nil, modelName, tokens), + } +} + +func addModelTokens(modelMap map[string]*model.ModelTokens, modelName string, tokens int64) { + if modelStat, ok := modelMap[modelName]; ok { + modelStat.Tokens += tokens + modelStat.Requests++ + return + } + + modelMap[modelName] = &model.ModelTokens{ + Model: modelName, + Tokens: tokens, + Requests: 1, + } +} + +func addDailyModelStat(models map[string]model.DailyModelStat, modelName string, tokens int64) map[string]model.DailyModelStat { + if models == nil { + models = make(map[string]model.DailyModelStat) + } + + modelStat := models[modelName] + modelStat.Tokens += tokens + modelStat.Requests++ + models[modelName] = modelStat + return models +} diff --git a/proxy/internal/service/storage_analytics_test.go b/proxy/internal/service/storage_analytics_test.go new file mode 100644 index 0000000..ff25299 --- /dev/null +++ b/proxy/internal/service/storage_analytics_test.go @@ -0,0 +1,83 @@ +package service + +import ( + "database/sql" + "encoding/json" + "testing" + + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +func TestApplyStoredResponseToSummary(t *testing.T) { + t.Parallel() + + summary := &model.RequestSummary{} + applyStoredResponseToSummary(summary, sql.NullString{ + Valid: true, + String: `{"statusCode":201,"responseTime":42,"body":{"usage":{"input_tokens":10,"output_tokens":5},"stop_reason":"end_turn"}}`, + }) + + if summary.StatusCode != 201 { + t.Fatalf("expected status code 201, got %d", summary.StatusCode) + } + if summary.ResponseTime != 42 { + t.Fatalf("expected response time 42, got %d", summary.ResponseTime) + } + if summary.Usage == nil || summary.Usage.InputTokens != 10 || summary.Usage.OutputTokens != 5 { + t.Fatalf("expected usage decoded, got %#v", summary.Usage) + } + if summary.StopReason != "end_turn" { + t.Fatalf("expected stop reason end_turn, got %q", summary.StopReason) + } +} + +func TestApplyStoredResponseToSummaryIgnoresInvalidPayload(t *testing.T) { + t.Parallel() + + summary := &model.RequestSummary{} + applyStoredResponseToSummary(summary, sql.NullString{Valid: true, String: `{not-json`}) + + if summary.StatusCode != 0 || summary.ResponseTime != 0 || summary.Usage != nil || summary.StopReason != "" { + t.Fatalf("expected invalid payload ignored, got %#v", summary) + } +} + +func TestDecodeResponseBodySummary(t *testing.T) { + t.Parallel() + + body, _ := json.Marshal(map[string]interface{}{ + "usage": map[string]interface{}{ + "input_tokens": 7, + "output_tokens": 3, + "cache_read_input_tokens": 2, + "cache_creation_input_tokens": 1, + }, + "stop_reason": "tool_use", + }) + + summary, ok := decodeResponseBodySummary(body) + if !ok { + t.Fatal("expected response body summary to decode") + } + if total := totalTokensFromUsage(summary.Usage); total != 13 { + t.Fatalf("expected total tokens 13, got %d", total) + } + if summary.StopReason != "tool_use" { + t.Fatalf("expected stop reason tool_use, got %q", summary.StopReason) + } +} + +func TestUsageCountsFromStoredResponse(t *testing.T) { + t.Parallel() + + input, output, cache, ok := usageCountsFromStoredResponse(sql.NullString{ + Valid: true, + String: `{"body":{"usage":{"input_tokens":7,"output_tokens":3,"cache_read_input_tokens":2,"cache_creation_input_tokens":1}}}`, + }) + if !ok { + t.Fatal("expected usage counts to decode") + } + if input != 7 || output != 3 || cache != 3 { + t.Fatalf("unexpected usage counts: input=%d output=%d cache=%d", input, output, cache) + } +} diff --git a/proxy/internal/service/storage_contract_test.go b/proxy/internal/service/storage_contract_test.go new file mode 100644 index 0000000..d4b0c87 --- /dev/null +++ b/proxy/internal/service/storage_contract_test.go @@ -0,0 +1,217 @@ +package service + +import ( + "encoding/json" + "path/filepath" + "testing" + "time" + + "github.com/seifghazi/claude-code-monitor/internal/config" + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +type storageFactory struct { + name string + new func(t *testing.T, cfg config.StorageConfig) StorageService +} + +func runStorageContractTests(t *testing.T, factory storageFactory) { + t.Helper() + + t.Run("save and fetch by short id", func(t *testing.T) { + storage := factory.new(t, config.StorageConfig{ + DBPath: filepath.Join(t.TempDir(), factory.name+".db"), + }) + + req := newContractRequest("fetch-123") + mustSaveRequest(t, storage, req) + + got := mustGetByShortID(t, storage, "123") + if got.RequestID != req.RequestID { + t.Fatalf("expected request id %q, got %q", req.RequestID, got.RequestID) + } + if got.Method != req.Method || got.Endpoint != req.Endpoint || got.Model != req.Model { + t.Fatalf("unexpected fetched request: %#v", got) + } + }) + + t.Run("update response persists status and usage metadata", func(t *testing.T) { + storage := factory.new(t, config.StorageConfig{ + DBPath: filepath.Join(t.TempDir(), factory.name+".db"), + }) + + req := newContractRequest("response-123") + mustSaveRequest(t, storage, req) + req.Response = newContractResponse() + + if err := storage.UpdateRequestWithResponse(req); err != nil { + t.Fatalf("UpdateRequestWithResponse() error = %v", err) + } + + got := mustGetByShortID(t, storage, "123") + if got.Response == nil || got.Response.StatusCode != 200 { + t.Fatalf("expected stored response, got %#v", got.Response) + } + }) + + t.Run("redaction survives round trip", func(t *testing.T) { + storage := factory.new(t, config.StorageConfig{ + DBPath: filepath.Join(t.TempDir(), factory.name+".db"), + CaptureRequestBody: true, + CaptureResponseBody: true, + RedactedFields: []string{"api_key", "secret"}, + }) + + req := newContractRequest("redact-123") + req.Body = map[string]interface{}{ + "api_key": "abc123", + "nested": map[string]interface{}{ + "secret": "top-secret", + "keep": "ok", + }, + } + req.Response = &model.ResponseLog{ + StatusCode: httpStatusOK, + Headers: map[string][]string{"Content-Type": {"application/json"}}, + Body: json.RawMessage(`{"secret":"response-secret","visible":"yes"}`), + ResponseTime: 12, + CompletedAt: time.Now().UTC().Format(time.RFC3339), + } + + mustSaveRequest(t, storage, req) + if err := storage.UpdateRequestWithResponse(req); err != nil { + t.Fatalf("UpdateRequestWithResponse() error = %v", err) + } + + got := mustGetByShortID(t, storage, "123") + body := got.Body.(map[string]interface{}) + if body["api_key"] != redactionPlaceholder { + t.Fatalf("expected api_key redacted, got %#v", body["api_key"]) + } + nested := body["nested"].(map[string]interface{}) + if nested["secret"] != redactionPlaceholder || nested["keep"] != "ok" { + t.Fatalf("unexpected nested redaction result: %#v", nested) + } + }) + + t.Run("body suppression semantics", func(t *testing.T) { + storage := factory.new(t, config.StorageConfig{ + DBPath: filepath.Join(t.TempDir(), factory.name+".db"), + CaptureRequestBody: false, + CaptureResponseBody: false, + }) + + req := newContractRequest("suppress-123") + req.Body = map[string]interface{}{"message": "do not store me"} + req.Response = &model.ResponseLog{ + StatusCode: httpStatusOK, + Headers: map[string][]string{"Content-Type": {"application/json"}}, + Body: json.RawMessage(`{"answer":"do not store me"}`), + BodyText: "sensitive text", + StreamingChunks: []string{"data: chunk-1"}, + ResponseTime: 10, + CompletedAt: time.Now().UTC().Format(time.RFC3339), + } + + mustSaveRequest(t, storage, req) + if err := storage.UpdateRequestWithResponse(req); err != nil { + t.Fatalf("UpdateRequestWithResponse() error = %v", err) + } + + got := mustGetByShortID(t, storage, "123") + body := got.Body.(map[string]interface{}) + if body["_storage_mode"] != "request_body_disabled" { + t.Fatalf("expected request body placeholder, got %#v", body) + } + if got.Response == nil || len(got.Response.Body) != 0 || got.Response.BodyText != "" || len(got.Response.StreamingChunks) != 0 { + t.Fatalf("expected suppressed response body fields, got %#v", got.Response) + } + }) + + t.Run("retention cleanup on write", func(t *testing.T) { + storage := factory.new(t, config.StorageConfig{ + DBPath: filepath.Join(t.TempDir(), factory.name+".db"), + RetentionDays: 1, + }) + + oldReq := newContractRequest("old-123") + oldReq.Timestamp = time.Now().Add(-48 * time.Hour).UTC().Format(time.RFC3339) + mustSaveRequest(t, storage, oldReq) + + recentReq := newContractRequest("recent-123") + mustSaveRequest(t, storage, recentReq) + + got, err := storage.GetAllRequests("all") + if err != nil { + t.Fatalf("GetAllRequests() error = %v", err) + } + if len(got) != 1 || got[0].RequestID != "recent-123" { + t.Fatalf("expected only recent request to remain, got %#v", got) + } + }) + + t.Run("clear requests removes all rows", func(t *testing.T) { + storage := factory.new(t, config.StorageConfig{ + DBPath: filepath.Join(t.TempDir(), factory.name+".db"), + }) + + mustSaveRequest(t, storage, newContractRequest("clear-123")) + mustSaveRequest(t, storage, newContractRequest("clear-456")) + + deleted, err := storage.ClearRequests() + if err != nil { + t.Fatalf("ClearRequests() error = %v", err) + } + if deleted != 2 { + t.Fatalf("expected 2 deleted rows, got %d", deleted) + } + + got, err := storage.GetAllRequests("all") + if err != nil { + t.Fatalf("GetAllRequests() error = %v", err) + } + if len(got) != 0 { + t.Fatalf("expected no remaining requests, got %d", len(got)) + } + }) +} + +func newContractRequest(id string) *model.RequestLog { + return &model.RequestLog{ + RequestID: id, + Timestamp: time.Now().UTC().Format(time.RFC3339), + Method: "POST", + Endpoint: "/v1/messages", + Headers: map[string][]string{"Content-Type": {"application/json"}}, + Body: map[string]interface{}{"message": "hello"}, + Model: "claude-3-5-sonnet", + UserAgent: "test", + ContentType: "application/json", + } +} + +func newContractResponse() *model.ResponseLog { + return &model.ResponseLog{ + StatusCode: httpStatusOK, + Headers: map[string][]string{"Content-Type": {"application/json"}}, + Body: json.RawMessage(`{"usage":{"input_tokens":11,"output_tokens":22},"stop_reason":"end_turn"}`), + ResponseTime: 17, + CompletedAt: time.Now().UTC().Format(time.RFC3339), + } +} + +func mustSaveRequest(t *testing.T, storage StorageService, req *model.RequestLog) { + t.Helper() + if _, err := storage.SaveRequest(req); err != nil { + t.Fatalf("SaveRequest() error = %v", err) + } +} + +func mustGetByShortID(t *testing.T, storage StorageService, shortID string) *model.RequestLog { + t.Helper() + got, _, err := storage.GetRequestByShortID(shortID) + if err != nil { + t.Fatalf("GetRequestByShortID(%q) error = %v", shortID, err) + } + return got +} diff --git a/proxy/internal/service/storage_decode.go b/proxy/internal/service/storage_decode.go new file mode 100644 index 0000000..b8d2a79 --- /dev/null +++ b/proxy/internal/service/storage_decode.go @@ -0,0 +1,46 @@ +package service + +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +func unmarshalStoredRequestFields(logger *log.Logger, req *model.RequestLog, headersJSON, bodyJSON string, promptGradeJSON, responseJSON sql.NullString) error { + if err := json.Unmarshal([]byte(headersJSON), &req.Headers); err != nil { + return fmt.Errorf("failed to unmarshal headers: %w", err) + } + + var body interface{} + if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil { + return fmt.Errorf("failed to unmarshal body: %w", err) + } + req.Body = body + + if promptGradeJSON.Valid { + var grade model.PromptGrade + if err := json.Unmarshal([]byte(promptGradeJSON.String), &grade); err != nil { + if logger != nil { + logger.Printf("Warning: failed to unmarshal prompt grade: %v", err) + } + } else { + req.PromptGrade = &grade + } + } + + if responseJSON.Valid { + var resp model.ResponseLog + if err := json.Unmarshal([]byte(responseJSON.String), &resp); err != nil { + if logger != nil { + logger.Printf("Warning: failed to unmarshal response: %v", err) + } + } else { + req.Response = &resp + } + } + + return nil +} diff --git a/proxy/internal/service/storage_decode_test.go b/proxy/internal/service/storage_decode_test.go new file mode 100644 index 0000000..78b74dc --- /dev/null +++ b/proxy/internal/service/storage_decode_test.go @@ -0,0 +1,104 @@ +package service + +import ( + "database/sql" + "io" + "log" + "testing" + + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +func TestUnmarshalStoredRequestFields(t *testing.T) { + t.Parallel() + + logger := log.New(io.Discard, "", 0) + + t.Run("decodes all stored fields", func(t *testing.T) { + t.Parallel() + + req := &model.RequestLog{} + err := unmarshalStoredRequestFields( + logger, + req, + `{"content-type":["application/json"]}`, + `{"messages":[{"role":"user","content":"hi"}]}`, + sql.NullString{String: `{"score":8,"maxScore":10}`, Valid: true}, + sql.NullString{String: `{"statusCode":200,"body":{"ok":true}}`, Valid: true}, + ) + if err != nil { + t.Fatalf("unmarshalStoredRequestFields() error = %v", err) + } + if got := req.Headers["content-type"][0]; got != "application/json" { + t.Fatalf("expected header decoded, got %q", got) + } + body := req.Body.(map[string]interface{}) + messages := body["messages"].([]interface{}) + if len(messages) != 1 { + t.Fatalf("expected one message, got %#v", body) + } + if req.PromptGrade == nil || req.PromptGrade.Score != 8 { + t.Fatalf("expected prompt grade decoded, got %#v", req.PromptGrade) + } + if req.Response == nil || req.Response.StatusCode != 200 { + t.Fatalf("expected response decoded, got %#v", req.Response) + } + }) + + t.Run("returns error for invalid headers json", func(t *testing.T) { + t.Parallel() + + req := &model.RequestLog{} + err := unmarshalStoredRequestFields( + logger, + req, + `{not-json`, + `{"ok":true}`, + sql.NullString{}, + sql.NullString{}, + ) + if err == nil { + t.Fatal("expected invalid headers json error") + } + }) + + t.Run("returns error for invalid body json", func(t *testing.T) { + t.Parallel() + + req := &model.RequestLog{} + err := unmarshalStoredRequestFields( + logger, + req, + `{"accept":["application/json"]}`, + `{not-json`, + sql.NullString{}, + sql.NullString{}, + ) + if err == nil { + t.Fatal("expected invalid body json error") + } + }) + + t.Run("ignores corrupt optional grade and response", func(t *testing.T) { + t.Parallel() + + req := &model.RequestLog{} + err := unmarshalStoredRequestFields( + logger, + req, + `{"accept":["application/json"]}`, + `{"ok":true}`, + sql.NullString{String: `{"score":`, Valid: true}, + sql.NullString{String: `{"statusCode":`, Valid: true}, + ) + if err != nil { + t.Fatalf("unmarshalStoredRequestFields() unexpected error = %v", err) + } + if req.PromptGrade != nil { + t.Fatalf("expected corrupt prompt grade ignored, got %#v", req.PromptGrade) + } + if req.Response != nil { + t.Fatalf("expected corrupt response ignored, got %#v", req.Response) + } + }) +} diff --git a/proxy/internal/service/storage_migrations.go b/proxy/internal/service/storage_migrations.go new file mode 100644 index 0000000..144fd96 --- /dev/null +++ b/proxy/internal/service/storage_migrations.go @@ -0,0 +1,25 @@ +package service + +import ( + "database/sql" + "fmt" + "strings" +) + +func runMigrations(db *sql.DB, statements []string, ignoreErr func(statement string, err error) bool) error { + for _, stmt := range statements { + if _, err := db.Exec(stmt); err != nil { + if ignoreErr != nil && ignoreErr(stmt, err) { + continue + } + return fmt.Errorf("failed to run migration %q: %w", stmt, err) + } + } + + return nil +} + +func ignoreSQLiteDuplicateColumn(statement string, err error) bool { + return strings.Contains(statement, "ALTER TABLE requests ADD COLUMN") && + strings.Contains(strings.ToLower(err.Error()), "duplicate column name") +} diff --git a/proxy/internal/service/storage_payload.go b/proxy/internal/service/storage_payload.go new file mode 100644 index 0000000..ffc6c6a --- /dev/null +++ b/proxy/internal/service/storage_payload.go @@ -0,0 +1,203 @@ +package service + +import ( + "encoding/json" + "log" + "strings" + + "github.com/seifghazi/claude-code-monitor/internal/config" + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +const redactionPlaceholder = "[REDACTED]" + +// maxStoredBodyBytes is the maximum serialized size of a request body stored in the DB. +// Bodies larger than this (e.g. 1M context payloads) are replaced with a metadata summary. +const maxStoredBodyBytes = 512 * 1024 // 512 KB + +func prepareRequestBodyForStorage(cfg *config.StorageConfig, body interface{}) (interface{}, error) { + if shouldSuppressBodies(cfg) { + return storageBodyPlaceholder("metadata_only"), nil + } + if cfg != nil && !cfg.CaptureRequestBody { + return storageBodyPlaceholder("request_body_disabled"), nil + } + + normalized, err := normalizeJSONValue(body) + if err != nil { + return nil, err + } + + redacted := redactJSONValue(normalized, redactedFieldSet(redactedFields(cfg))) + + // Check serialized size; if too large, store a lightweight summary instead + data, err := json.Marshal(redacted) + if err != nil { + return redacted, nil + } + if len(data) > maxStoredBodyBytes { + return truncatedBodySummary(redacted, len(data)), nil + } + + return redacted, nil +} + +// truncatedBodySummary extracts key metadata from an oversized request body +// so the DB row stays small while retaining useful diagnostic info. +func truncatedBodySummary(body interface{}, originalBytes int) map[string]interface{} { + summary := map[string]interface{}{ + "_truncated": true, + "_original_bytes": originalBytes, + } + if m, ok := body.(map[string]interface{}); ok { + if v, ok := m["model"]; ok { + summary["model"] = v + } + if v, ok := m["stream"]; ok { + summary["stream"] = v + } + if v, ok := m["max_tokens"]; ok { + summary["max_tokens"] = v + } + if msgs, ok := m["messages"].([]interface{}); ok { + summary["message_count"] = len(msgs) + } + if sys, ok := m["system"].([]interface{}); ok { + summary["system_count"] = len(sys) + } + if tools, ok := m["tools"].([]interface{}); ok { + summary["tool_count"] = len(tools) + } + } + return summary +} + +func prepareResponseForStorage(cfg *config.StorageConfig, logger *log.Logger, response *model.ResponseLog) (*model.ResponseLog, error) { + if response == nil { + return nil, nil + } + + clone := *response + if shouldSuppressBodies(cfg) || (cfg != nil && !cfg.CaptureResponseBody) { + clone.Body = nil + clone.BodyText = "" + clone.StreamingChunks = nil + clone.ChunkTimings = nil + return &clone, nil + } + + if len(clone.Body) > 0 { + sanitizedBody, err := sanitizeRawJSON(clone.Body, redactedFieldSet(redactedFields(cfg))) + if err != nil { + if logger != nil { + logger.Printf("Warning: failed to redact response body: %v", err) + } + } else { + clone.Body = sanitizedBody + } + // Cap stored response body size + if len(clone.Body) > maxStoredBodyBytes { + clone.Body = json.RawMessage(`{"_truncated":true}`) + } + } + + // Cap stored streaming chunks to avoid huge DB rows on long streams + const maxStoredChunks = 500 + if len(clone.StreamingChunks) > maxStoredChunks { + clone.StreamingChunks = clone.StreamingChunks[:maxStoredChunks] + } + if len(clone.ChunkTimings) > maxStoredChunks { + clone.ChunkTimings = clone.ChunkTimings[:maxStoredChunks] + } + + return &clone, nil +} + +func shouldSuppressBodies(cfg *config.StorageConfig) bool { + return cfg != nil && cfg.MetadataOnly +} + +func redactedFields(cfg *config.StorageConfig) []string { + if cfg == nil { + return nil + } + return cfg.RedactedFields +} + +func normalizeJSONValue(value interface{}) (interface{}, error) { + if value == nil { + return nil, nil + } + + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + + var normalized interface{} + if err := json.Unmarshal(data, &normalized); err != nil { + return nil, err + } + + return normalized, nil +} + +func sanitizeRawJSON(raw json.RawMessage, redacted map[string]struct{}) (json.RawMessage, error) { + if len(raw) == 0 { + return raw, nil + } + + var value interface{} + if err := json.Unmarshal(raw, &value); err != nil { + return raw, err + } + + sanitized := redactJSONValue(value, redacted) + data, err := json.Marshal(sanitized) + if err != nil { + return raw, err + } + + return json.RawMessage(data), nil +} + +func redactJSONValue(value interface{}, redacted map[string]struct{}) interface{} { + switch typed := value.(type) { + case map[string]interface{}: + result := make(map[string]interface{}, len(typed)) + for key, child := range typed { + if _, ok := redacted[strings.ToLower(key)]; ok { + result[key] = redactionPlaceholder + continue + } + result[key] = redactJSONValue(child, redacted) + } + return result + case []interface{}: + result := make([]interface{}, len(typed)) + for i, child := range typed { + result[i] = redactJSONValue(child, redacted) + } + return result + default: + return value + } +} + +func storageBodyPlaceholder(mode string) map[string]interface{} { + return map[string]interface{}{ + "_storage_mode": mode, + } +} + +func redactedFieldSet(fields []string) map[string]struct{} { + set := make(map[string]struct{}, len(fields)) + for _, field := range fields { + field = strings.TrimSpace(strings.ToLower(field)) + if field == "" { + continue + } + set[field] = struct{}{} + } + return set +} diff --git a/proxy/internal/service/storage_payload_test.go b/proxy/internal/service/storage_payload_test.go new file mode 100644 index 0000000..3fe1933 --- /dev/null +++ b/proxy/internal/service/storage_payload_test.go @@ -0,0 +1,163 @@ +package service + +import ( + "encoding/json" + "io" + "log" + "testing" + + "github.com/seifghazi/claude-code-monitor/internal/config" + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +func TestPrepareRequestBodyForStorage(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cfg *config.StorageConfig + body interface{} + assert func(t *testing.T, got interface{}) + }{ + { + name: "metadata only returns placeholder", + cfg: &config.StorageConfig{ + MetadataOnly: true, + }, + body: map[string]interface{}{"secret": "value"}, + assert: func(t *testing.T, got interface{}) { + t.Helper() + body, ok := got.(map[string]interface{}) + if !ok || body["_storage_mode"] != "metadata_only" { + t.Fatalf("expected metadata_only placeholder, got %#v", got) + } + }, + }, + { + name: "request capture disabled returns placeholder", + cfg: &config.StorageConfig{ + CaptureRequestBody: false, + }, + body: map[string]interface{}{"secret": "value"}, + assert: func(t *testing.T, got interface{}) { + t.Helper() + body, ok := got.(map[string]interface{}) + if !ok || body["_storage_mode"] != "request_body_disabled" { + t.Fatalf("expected request_body_disabled placeholder, got %#v", got) + } + }, + }, + { + name: "redacts nested fields", + cfg: &config.StorageConfig{ + CaptureRequestBody: true, + RedactedFields: []string{"authorization", "password"}, + }, + body: map[string]interface{}{ + "authorization": "top-secret", + "nested": map[string]interface{}{ + "password": "hide-me", + "keep": "visible", + }, + }, + assert: func(t *testing.T, got interface{}) { + t.Helper() + body := got.(map[string]interface{}) + if body["authorization"] != redactionPlaceholder { + t.Fatalf("expected top-level field redacted, got %#v", body["authorization"]) + } + nested := body["nested"].(map[string]interface{}) + if nested["password"] != redactionPlaceholder { + t.Fatalf("expected nested field redacted, got %#v", nested["password"]) + } + if nested["keep"] != "visible" { + t.Fatalf("expected keep field preserved, got %#v", nested["keep"]) + } + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := prepareRequestBodyForStorage(tt.cfg, tt.body) + if err != nil { + t.Fatalf("prepareRequestBodyForStorage() error = %v", err) + } + + tt.assert(t, got) + }) + } +} + +func TestPrepareResponseForStorage(t *testing.T) { + t.Parallel() + + logger := log.New(io.Discard, "", 0) + + t.Run("metadata only strips body fields", func(t *testing.T) { + t.Parallel() + + got, err := prepareResponseForStorage(&config.StorageConfig{MetadataOnly: true}, logger, &model.ResponseLog{ + Body: json.RawMessage(`{"secret":"value"}`), + BodyText: "raw", + StreamingChunks: []string{"chunk"}, + ChunkTimings: []model.ChunkTiming{{Index: 0}}, + }) + if err != nil { + t.Fatalf("prepareResponseForStorage() error = %v", err) + } + if got == nil { + t.Fatal("expected response clone") + } + if got.Body != nil || got.BodyText != "" || got.StreamingChunks != nil || got.ChunkTimings != nil { + t.Fatalf("expected body fields stripped, got %#v", got) + } + }) + + t.Run("redacts json response bodies", func(t *testing.T) { + t.Parallel() + + got, err := prepareResponseForStorage(&config.StorageConfig{ + CaptureResponseBody: true, + RedactedFields: []string{"api_key"}, + }, logger, &model.ResponseLog{ + Body: json.RawMessage(`{"api_key":"secret","nested":{"keep":"ok"}}`), + }) + if err != nil { + t.Fatalf("prepareResponseForStorage() error = %v", err) + } + + var body map[string]interface{} + if err := json.Unmarshal(got.Body, &body); err != nil { + t.Fatalf("unmarshal redacted body: %v", err) + } + if body["api_key"] != redactionPlaceholder { + t.Fatalf("expected api_key redacted, got %#v", body["api_key"]) + } + nested := body["nested"].(map[string]interface{}) + if nested["keep"] != "ok" { + t.Fatalf("expected nested field preserved, got %#v", nested["keep"]) + } + }) + + t.Run("preserves non json body bytes", func(t *testing.T) { + t.Parallel() + + original := json.RawMessage(`not-json`) + got, err := prepareResponseForStorage(&config.StorageConfig{ + CaptureResponseBody: true, + RedactedFields: []string{"token"}, + }, logger, &model.ResponseLog{ + Body: original, + }) + if err != nil { + t.Fatalf("prepareResponseForStorage() error = %v", err) + } + if string(got.Body) != string(original) { + t.Fatalf("expected original non-json body preserved, got %q", string(got.Body)) + } + }) +} diff --git a/proxy/internal/service/storage_postgres.go b/proxy/internal/service/storage_postgres.go new file mode 100644 index 0000000..2614a66 --- /dev/null +++ b/proxy/internal/service/storage_postgres.go @@ -0,0 +1,1074 @@ +package service + +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + "sort" + "strings" + "time" + + _ "github.com/lib/pq" + + "github.com/seifghazi/claude-code-monitor/internal/config" + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +type postgresStorageService struct { + db *sql.DB + config *config.StorageConfig + logger *log.Logger + + // Prepared statements for frequently used queries + stmtInsertRequest *sql.Stmt + stmtUpdateResponse *sql.Stmt + stmtUpdateGrading *sql.Stmt + stmtGetRequestByID *sql.Stmt + stmtGetRequestsPage *sql.Stmt + stmtGetRequestsCount *sql.Stmt + stmtDeleteOldRequests *sql.Stmt +} + +func NewPostgresStorageService(cfg *config.StorageConfig) (StorageService, error) { + return NewPostgresStorageServiceWithLogger(cfg, log.Default()) +} + +func NewPostgresStorageServiceWithLogger(cfg *config.StorageConfig, logger *log.Logger) (StorageService, error) { + db, err := sql.Open("postgres", cfg.DatabaseURL) + if err != nil { + return nil, fmt.Errorf("failed to open postgres database: %w", err) + } + + // Configure connection pool — PostgreSQL handles concurrency well + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(5) + db.SetConnMaxLifetime(5 * time.Minute) + + // Verify connection + if err := db.Ping(); err != nil { + db.Close() + return nil, fmt.Errorf("failed to ping postgres database: %w", err) + } + + service := &postgresStorageService{ + db: db, + config: cfg, + logger: logger, + } + + if err := service.createTables(); err != nil { + db.Close() + return nil, fmt.Errorf("failed to create tables: %w", err) + } + + if err := service.prepareStatements(); err != nil { + db.Close() + return nil, fmt.Errorf("failed to prepare statements: %w", err) + } + + if err := service.cleanupExpiredRequests(); err != nil { + logger.Printf("Warning: failed to apply retention policy during startup: %v", err) + } + + return service, nil +} + +func (s *postgresStorageService) createTables() error { + schema := ` + CREATE TABLE IF NOT EXISTS requests ( + id TEXT PRIMARY KEY, + timestamp TIMESTAMPTZ NOT NULL, + method TEXT NOT NULL, + endpoint TEXT NOT NULL, + headers TEXT NOT NULL, + body TEXT NOT NULL, + user_agent TEXT, + content_type TEXT, + prompt_grade TEXT, + response TEXT, + model TEXT, + original_model TEXT, + routed_model TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp DESC); + CREATE INDEX IF NOT EXISTS idx_requests_model ON requests(model); + CREATE INDEX IF NOT EXISTS idx_requests_endpoint ON requests(endpoint); + ` + + _, err := s.db.Exec(schema) + if err != nil { + return err + } + + return runMigrations(s.db, []string{ + "ALTER TABLE requests ADD COLUMN IF NOT EXISTS conversation_hash TEXT", + "ALTER TABLE requests ADD COLUMN IF NOT EXISTS message_count INTEGER DEFAULT 0", + "CREATE INDEX IF NOT EXISTS idx_requests_conversation_hash ON requests(conversation_hash)", + "ALTER TABLE requests ADD COLUMN IF NOT EXISTS organization_id TEXT", + "CREATE INDEX IF NOT EXISTS idx_requests_organization_id ON requests(organization_id)", + `CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)`, + }, nil) +} + +func (s *postgresStorageService) prepareStatements() error { + var err error + + s.stmtInsertRequest, err = s.db.Prepare(` + INSERT INTO requests (id, timestamp, method, endpoint, headers, body, user_agent, content_type, model, original_model, routed_model, conversation_hash, message_count) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + `) + if err != nil { + return fmt.Errorf("failed to prepare insert statement: %w", err) + } + + s.stmtUpdateResponse, err = s.db.Prepare(` + UPDATE requests SET response = $1, organization_id = COALESCE(NULLIF($3, ''), organization_id) WHERE id = $2 + `) + if err != nil { + return fmt.Errorf("failed to prepare update response statement: %w", err) + } + + s.stmtUpdateGrading, err = s.db.Prepare(` + UPDATE requests SET prompt_grade = $1 WHERE id = $2 + `) + if err != nil { + return fmt.Errorf("failed to prepare update grading statement: %w", err) + } + + s.stmtGetRequestByID, err = s.db.Prepare(` + SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model + FROM requests + WHERE id = $1 + `) + if err != nil { + return fmt.Errorf("failed to prepare get by ID statement: %w", err) + } + + s.stmtGetRequestsPage, err = s.db.Prepare(` + SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model + FROM requests + ORDER BY timestamp DESC + LIMIT $1 OFFSET $2 + `) + if err != nil { + return fmt.Errorf("failed to prepare get requests page statement: %w", err) + } + + s.stmtGetRequestsCount, err = s.db.Prepare(` + SELECT COUNT(*) FROM requests + `) + if err != nil { + return fmt.Errorf("failed to prepare count statement: %w", err) + } + + s.stmtDeleteOldRequests, err = s.db.Prepare(` + DELETE FROM requests WHERE timestamp < $1 + `) + if err != nil { + return fmt.Errorf("failed to prepare delete old requests statement: %w", err) + } + + return nil +} + +func (s *postgresStorageService) SaveRequest(request *model.RequestLog) (string, error) { + headersJSON, err := json.Marshal(request.Headers) + if err != nil { + return "", fmt.Errorf("failed to marshal headers: %w", err) + } + + bodyForStorage, err := prepareRequestBodyForStorage(s.config, request.Body) + if err != nil { + return "", fmt.Errorf("failed to prepare body for storage: %w", err) + } + + bodyJSON, err := json.Marshal(bodyForStorage) + if err != nil { + return "", fmt.Errorf("failed to marshal body: %w", err) + } + + _, err = s.stmtInsertRequest.Exec( + request.RequestID, + request.Timestamp, + request.Method, + request.Endpoint, + string(headersJSON), + string(bodyJSON), + request.UserAgent, + request.ContentType, + request.Model, + request.OriginalModel, + request.RoutedModel, + request.ConversationHash, + request.MessageCount, + ) + + if err != nil { + return "", fmt.Errorf("failed to insert request: %w", err) + } + + if err := s.cleanupExpiredRequests(); err != nil { + s.logger.Printf("Warning: failed to apply retention policy: %v", err) + } + + return request.RequestID, nil +} + +func (s *postgresStorageService) GetRequests(page, limit int, modelFilter string) ([]model.RequestLog, int, error) { + whereClause := "" + countArgs := []interface{}{} + queryArgs := []interface{}{} + argIdx := 1 + + if filterValue, ok := modelFilterPattern(modelFilter, escapePostgresLikePattern); ok { + whereClause = fmt.Sprintf(" WHERE LOWER(model) LIKE $%d", argIdx) + countArgs = append(countArgs, filterValue) + queryArgs = append(queryArgs, filterValue) + argIdx++ + } + + // Get total count + var total int + countQuery := "SELECT COUNT(*) FROM requests" + whereClause + err := s.db.QueryRow(countQuery, countArgs...).Scan(&total) + if err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Get paginated results + offset := (page - 1) * limit + query := fmt.Sprintf(` + SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model + FROM requests%s + ORDER BY timestamp DESC + LIMIT $%d OFFSET $%d + `, whereClause, argIdx, argIdx+1) + queryArgs = append(queryArgs, limit, offset) + + rows, err := s.db.Query(query, queryArgs...) + if err != nil { + return nil, 0, fmt.Errorf("failed to query requests: %w", err) + } + defer rows.Close() + + requests, err := s.scanRequestRows(rows) + if err != nil { + return nil, 0, err + } + + return requests, total, nil +} + +func (s *postgresStorageService) ClearRequests() (int, error) { + result, err := s.db.Exec("DELETE FROM requests") + if err != nil { + return 0, fmt.Errorf("failed to clear requests: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return 0, fmt.Errorf("failed to get rows affected: %w", err) + } + + return int(rowsAffected), nil +} + +func (s *postgresStorageService) UpdateRequestWithGrading(requestID string, grade *model.PromptGrade) error { + gradeJSON, err := json.Marshal(grade) + if err != nil { + return fmt.Errorf("failed to marshal grade: %w", err) + } + + result, err := s.stmtUpdateGrading.Exec(string(gradeJSON), requestID) + if err != nil { + return fmt.Errorf("failed to update request with grading: %w", err) + } + + rowsAffected, _ := result.RowsAffected() + if rowsAffected == 0 { + return fmt.Errorf("request %s not found", requestID) + } + + if err := s.cleanupExpiredRequests(); err != nil { + s.logger.Printf("Warning: failed to apply retention policy: %v", err) + } + + return nil +} + +func (s *postgresStorageService) UpdateRequestWithResponse(request *model.RequestLog) error { + responseForStorage, err := prepareResponseForStorage(s.config, s.logger, request.Response) + if err != nil { + return fmt.Errorf("failed to prepare response for storage: %w", err) + } + + responseJSON, err := json.Marshal(responseForStorage) + if err != nil { + return fmt.Errorf("failed to marshal response: %w", err) + } + + orgID := request.OrganizationID + result, err := s.stmtUpdateResponse.Exec(string(responseJSON), request.RequestID, orgID) + if err != nil { + return fmt.Errorf("failed to update request with response: %w", err) + } + + rowsAffected, _ := result.RowsAffected() + if rowsAffected == 0 { + return fmt.Errorf("request %s not found", request.RequestID) + } + + return nil +} + +func (s *postgresStorageService) EnsureDirectoryExists() error { + return nil +} + +func (s *postgresStorageService) GetRequestByShortID(shortID string) (*model.RequestLog, string, error) { + escapedID := escapePostgresLikePattern(shortID) + + query := ` + SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model + FROM requests + WHERE id LIKE $1 + ORDER BY timestamp DESC + LIMIT 1 + ` + + var req model.RequestLog + var headersJSON, bodyJSON string + var promptGradeJSON, responseJSON sql.NullString + var timestamp time.Time + + err := s.db.QueryRow(query, "%"+escapedID).Scan( + &req.RequestID, + ×tamp, + &req.Method, + &req.Endpoint, + &headersJSON, + &bodyJSON, + &req.Model, + &req.UserAgent, + &req.ContentType, + &promptGradeJSON, + &responseJSON, + &req.OriginalModel, + &req.RoutedModel, + ) + + if err == sql.ErrNoRows { + return nil, "", fmt.Errorf("request with ID %s not found", shortID) + } + if err != nil { + return nil, "", fmt.Errorf("failed to query request: %w", err) + } + + req.Timestamp = timestamp.Format(time.RFC3339) + + if err := unmarshalStoredRequestFields(s.logger, &req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil { + return nil, "", err + } + + return &req, req.RequestID, nil +} + +func (s *postgresStorageService) GetConfig() *config.StorageConfig { + return s.config +} + +func (s *postgresStorageService) GetAllRequests(modelFilter string) ([]*model.RequestLog, error) { + return s.getAllRequestsWithLimit(modelFilter, 0) +} + +func (s *postgresStorageService) getAllRequestsWithLimit(modelFilter string, limit int) ([]*model.RequestLog, error) { + var query string + args := []interface{}{} + argIdx := 1 + + if filterValue, ok := modelFilterPattern(modelFilter, escapePostgresLikePattern); ok { + query = fmt.Sprintf(` + SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model + FROM requests + WHERE LOWER(model) LIKE $%d + ORDER BY timestamp DESC + `, argIdx) + args = append(args, filterValue) + argIdx++ + } else { + query = ` + SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model + FROM requests + ORDER BY timestamp DESC + ` + } + + if limit > 0 { + query += fmt.Sprintf(" LIMIT $%d", argIdx) + args = append(args, limit) + } + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query requests: %w", err) + } + defer rows.Close() + + var requests []*model.RequestLog + for rows.Next() { + req, err := s.scanSingleRow(rows) + if err != nil { + s.logger.Printf("Warning: failed to scan request row: %v", err) + continue + } + requests = append(requests, req) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating rows: %w", err) + } + + return requests, nil +} + +func (s *postgresStorageService) DeleteRequestsOlderThan(age time.Duration) (int, error) { + cutoff := time.Now().Add(-age) + + result, err := s.stmtDeleteOldRequests.Exec(cutoff.Format(time.RFC3339)) + if err != nil { + return 0, fmt.Errorf("failed to delete old requests: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return 0, fmt.Errorf("failed to get rows affected: %w", err) + } + + return int(rowsAffected), nil +} + +func (s *postgresStorageService) GetDatabaseStats() (map[string]interface{}, error) { + stats := make(map[string]interface{}) + + // Get row count + var count int + err := s.stmtGetRequestsCount.QueryRow().Scan(&count) + if err != nil { + return nil, fmt.Errorf("failed to get count: %w", err) + } + stats["total_requests"] = count + + // Get database size + var dbSize int64 + err = s.db.QueryRow("SELECT pg_database_size(current_database())").Scan(&dbSize) + if err == nil { + stats["database_size_bytes"] = dbSize + } + + // Get oldest and newest timestamps + var oldest, newest sql.NullTime + err = s.db.QueryRow("SELECT MIN(timestamp), MAX(timestamp) FROM requests").Scan(&oldest, &newest) + if err == nil { + if oldest.Valid { + stats["oldest_request"] = oldest.Time.Format(time.RFC3339) + } + if newest.Valid { + stats["newest_request"] = newest.Time.Format(time.RFC3339) + } + } + + return stats, nil +} + +func (s *postgresStorageService) Close() error { + if s.stmtInsertRequest != nil { + s.stmtInsertRequest.Close() + } + if s.stmtUpdateResponse != nil { + s.stmtUpdateResponse.Close() + } + if s.stmtUpdateGrading != nil { + s.stmtUpdateGrading.Close() + } + if s.stmtGetRequestByID != nil { + s.stmtGetRequestByID.Close() + } + if s.stmtGetRequestsPage != nil { + s.stmtGetRequestsPage.Close() + } + if s.stmtGetRequestsCount != nil { + s.stmtGetRequestsCount.Close() + } + if s.stmtDeleteOldRequests != nil { + s.stmtDeleteOldRequests.Close() + } + + return s.db.Close() +} + +// Helper functions + +// escapePostgresLikePattern escapes special characters in LIKE patterns for PostgreSQL +func escapePostgresLikePattern(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + s = strings.ReplaceAll(s, `%`, `\%`) + s = strings.ReplaceAll(s, `_`, `\_`) + return s +} + +func (s *postgresStorageService) scanRequestRows(rows *sql.Rows) ([]model.RequestLog, error) { + var requests []model.RequestLog + + for rows.Next() { + var req model.RequestLog + var headersJSON, bodyJSON string + var promptGradeJSON, responseJSON sql.NullString + var timestamp time.Time + + err := rows.Scan( + &req.RequestID, + ×tamp, + &req.Method, + &req.Endpoint, + &headersJSON, + &bodyJSON, + &req.Model, + &req.UserAgent, + &req.ContentType, + &promptGradeJSON, + &responseJSON, + &req.OriginalModel, + &req.RoutedModel, + ) + if err != nil { + s.logger.Printf("Warning: failed to scan row: %v", err) + continue + } + + req.Timestamp = timestamp.Format(time.RFC3339) + + if err := unmarshalStoredRequestFields(s.logger, &req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil { + s.logger.Printf("Warning: failed to unmarshal request fields: %v", err) + continue + } + + requests = append(requests, req) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating rows: %w", err) + } + + return requests, nil +} + +func (s *postgresStorageService) scanSingleRow(rows *sql.Rows) (*model.RequestLog, error) { + var req model.RequestLog + var headersJSON, bodyJSON string + var promptGradeJSON, responseJSON sql.NullString + var timestamp time.Time + + err := rows.Scan( + &req.RequestID, + ×tamp, + &req.Method, + &req.Endpoint, + &headersJSON, + &bodyJSON, + &req.Model, + &req.UserAgent, + &req.ContentType, + &promptGradeJSON, + &responseJSON, + &req.OriginalModel, + &req.RoutedModel, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + + req.Timestamp = timestamp.Format(time.RFC3339) + + if err := unmarshalStoredRequestFields(s.logger, &req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil { + return nil, err + } + + return &req, nil +} + +func (s *postgresStorageService) cleanupExpiredRequests() error { + if s.config == nil || s.config.RetentionDays <= 0 { + return nil + } + + _, err := s.DeleteRequestsOlderThan(time.Duration(s.config.RetentionDays) * 24 * time.Hour) + return err +} + +// GetUsageStats returns aggregated token usage statistics +func (s *postgresStorageService) GetUsageStats(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) { + stats := &model.UsageStats{ + RequestsByModel: make(map[string]model.ModelStats), + } + + whereClause := "WHERE response IS NOT NULL" + args := []interface{}{} + argIdx := 1 + + if startDate != "" { + whereClause += fmt.Sprintf(" AND timestamp >= $%d", argIdx) + args = append(args, startDate) + argIdx++ + stats.StartDate = startDate + } + + if endDate != "" { + whereClause += fmt.Sprintf(" AND timestamp <= $%d", argIdx) + args = append(args, endDate) + argIdx++ + stats.EndDate = endDate + } + + if filterValue, ok := modelFilterPattern(modelFilter, escapePostgresLikePattern); ok { + whereClause += fmt.Sprintf(" AND LOWER(model) LIKE $%d", argIdx) + args = append(args, filterValue) + argIdx++ + } + + if orgFilter != "" { + whereClause += fmt.Sprintf(" AND organization_id = $%d", argIdx) + args = append(args, orgFilter) + argIdx++ + } + + query := ` + SELECT model, response + FROM requests + ` + whereClause + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query usage stats: %w", err) + } + defer rows.Close() + + for rows.Next() { + var modelName string + var responseJSON sql.NullString + + if err := rows.Scan(&modelName, &responseJSON); err != nil { + s.logger.Printf("Warning: failed to scan usage row: %v", err) + continue + } + + resp, ok := decodeStoredResponse(responseJSON) + if !ok { + continue + } + bodySummary, ok := decodeResponseBodySummary(resp.Body) + if !ok || bodySummary.Usage == nil { + continue + } + + addUsageStats(stats, modelName, bodySummary.Usage) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating usage rows: %w", err) + } + + if stats.StartDate == "" || stats.EndDate == "" { + var oldest, newest sql.NullTime + err := s.db.QueryRow("SELECT MIN(timestamp), MAX(timestamp) FROM requests WHERE response IS NOT NULL").Scan(&oldest, &newest) + if err == nil { + if stats.StartDate == "" && oldest.Valid { + stats.StartDate = oldest.Time.Format(time.RFC3339) + } + if stats.EndDate == "" && newest.Valid { + stats.EndDate = newest.Time.Format(time.RFC3339) + } + } + } + + return stats, nil +} + +// GetRequestsSummary returns minimal data for list view +func (s *postgresStorageService) GetRequestsSummary(modelFilter string) ([]*model.RequestSummary, error) { + query := ` + SELECT id, timestamp, method, endpoint, model, original_model, routed_model, response, COALESCE(conversation_hash, ''), COALESCE(message_count, 0) + FROM requests + ` + args := []interface{}{} + + if filterValue, ok := modelFilterPattern(modelFilter, escapePostgresLikePattern); ok { + query += " WHERE LOWER(model) LIKE $1" + args = append(args, filterValue) + } + + query += " ORDER BY timestamp DESC" + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query requests: %w", err) + } + defer rows.Close() + + return s.scanSummaryRows(rows) +} + +// GetRequestsSummaryPaginated returns minimal data for list view with pagination +func (s *postgresStorageService) GetRequestsSummaryPaginated(modelFilter, startTime, endTime string, offset, limit int) ([]*model.RequestSummary, int, error) { + whereClauses := []string{} + args := []interface{}{} + argIdx := 1 + + if filterValue, ok := modelFilterPattern(modelFilter, escapePostgresLikePattern); ok { + whereClauses = append(whereClauses, fmt.Sprintf("LOWER(model) LIKE $%d", argIdx)) + args = append(args, filterValue) + argIdx++ + } + + if startTime != "" && endTime != "" { + whereClauses = append(whereClauses, fmt.Sprintf("timestamp >= $%d AND timestamp <= $%d", argIdx, argIdx+1)) + args = append(args, startTime, endTime) + argIdx += 2 + } + + whereClause := "" + if len(whereClauses) > 0 { + whereClause = " WHERE " + strings.Join(whereClauses, " AND ") + } + + // Get total count + var total int + countQuery := "SELECT COUNT(*) FROM requests" + whereClause + countArgs := make([]interface{}, len(args)) + copy(countArgs, args) + if err := s.db.QueryRow(countQuery, countArgs...).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Get the requested page + query := ` + SELECT id, timestamp, method, endpoint, model, original_model, routed_model, response, COALESCE(conversation_hash, ''), COALESCE(message_count, 0) + FROM requests + ` + whereClause + " ORDER BY timestamp DESC" + + if limit > 0 { + query += fmt.Sprintf(" LIMIT $%d OFFSET $%d", argIdx, argIdx+1) + args = append(args, limit, offset) + } else if offset > 0 { + query += fmt.Sprintf(" OFFSET $%d", argIdx) + args = append(args, offset) + } + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, 0, fmt.Errorf("failed to query requests: %w", err) + } + defer rows.Close() + + summaries, err := s.scanSummaryRows(rows) + if err != nil { + return nil, 0, err + } + + s.logger.Printf("GetRequestsSummaryPaginated: returned %d requests (total: %d, limit: %d, offset: %d)", len(summaries), total, limit, offset) + return summaries, total, nil +} + +func (s *postgresStorageService) scanSummaryRows(rows *sql.Rows) ([]*model.RequestSummary, error) { + var summaries []*model.RequestSummary + for rows.Next() { + var summary model.RequestSummary + var responseJSON sql.NullString + var timestamp time.Time + + err := rows.Scan( + &summary.RequestID, + ×tamp, + &summary.Method, + &summary.Endpoint, + &summary.Model, + &summary.OriginalModel, + &summary.RoutedModel, + &responseJSON, + &summary.ConversationHash, + &summary.MessageCount, + ) + if err != nil { + s.logger.Printf("Warning: failed to scan summary row: %v", err) + continue + } + + summary.Timestamp = timestamp.Format(time.RFC3339) + + applyStoredResponseToSummary(&summary, responseJSON) + + summaries = append(summaries, &summary) + } + return summaries, nil +} + +// GetStats returns aggregated statistics for the dashboard +func (s *postgresStorageService) GetStats(startDate, endDate, orgFilter string) (*model.DashboardStats, error) { + stats := &model.DashboardStats{ + DailyStats: make([]model.DailyTokens, 0), + } + + query := ` + SELECT timestamp, COALESCE(model, 'unknown') as model, response + FROM requests + WHERE timestamp >= $1 AND timestamp <= $2 + ` + args := []interface{}{startDate, endDate} + if orgFilter != "" { + query += ` AND organization_id = $3` + args = append(args, orgFilter) + } + query += ` ORDER BY timestamp` + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query stats: %w", err) + } + defer rows.Close() + + dailyMap := make(map[string]*model.DailyTokens) + + for rows.Next() { + var timestamp time.Time + var modelName string + var responseJSON sql.NullString + + if err := rows.Scan(×tamp, &modelName, &responseJSON); err != nil { + continue + } + + date := timestamp.Format("2006-01-02") + + tokens := int64(0) + if resp, ok := decodeStoredResponse(responseJSON); ok { + if bodySummary, ok := decodeResponseBodySummary(resp.Body); ok { + tokens = totalTokensFromUsage(bodySummary.Usage) + } + } + + addDailyTokens(dailyMap, date, modelName, tokens) + } + + for _, v := range dailyMap { + stats.DailyStats = append(stats.DailyStats, *v) + } + + return stats, nil +} + +// GetHourlyStats returns time-bucketed breakdown for a specific time range. +// bucketMinutes controls the granularity (e.g. 5, 15, 30, 60). +func (s *postgresStorageService) GetHourlyStats(startTime, endTime string, bucketMinutes int, orgFilter string) (*model.HourlyStatsResponse, error) { + if bucketMinutes <= 0 { + bucketMinutes = 60 + } + + query := ` + SELECT timestamp, COALESCE(model, 'unknown') as model, response + FROM requests + WHERE timestamp >= $1 AND timestamp <= $2 + ` + args := []interface{}{startTime, endTime} + if orgFilter != "" { + query += ` AND organization_id = $3` + args = append(args, orgFilter) + } + query += ` ORDER BY timestamp` + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query hourly stats: %w", err) + } + defer rows.Close() + + bucketMap := make(map[string]*model.HourlyTokens) + var totalTokens int64 + var totalRequests int + var totalResponseTime int64 + var responseCount int + + for rows.Next() { + var timestamp time.Time + var modelName string + var responseJSON sql.NullString + + if err := rows.Scan(×tamp, &modelName, &responseJSON); err != nil { + continue + } + + // Always use absolute time buckets so multi-day ranges show per-slot data + var bucketKey, bucketLabel string + minuteOfDay := timestamp.Hour()*60 + timestamp.Minute() + bucketStart := (minuteOfDay / bucketMinutes) * bucketMinutes + bucketTime := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), bucketStart/60, bucketStart%60, 0, 0, timestamp.Location()) + bucketKey = bucketTime.Format("2006-01-02T15:04") + bucketLabel = bucketTime.Format("Jan 2 15:04") + + tokens := int64(0) + responseTime := int64(0) + if resp, ok := decodeStoredResponse(responseJSON); ok { + responseTime = resp.ResponseTime + if bodySummary, ok := decodeResponseBodySummary(resp.Body); ok { + tokens = totalTokensFromUsage(bodySummary.Usage) + } + } + + totalTokens += tokens + totalRequests++ + + if responseTime > 0 { + totalResponseTime += responseTime + responseCount++ + } + + addHourlyTokens(bucketMap, bucketKey, bucketLabel, modelName, tokens) + } + + // Convert map to sorted slice + keys := make([]string, 0, len(bucketMap)) + for k := range bucketMap { + keys = append(keys, k) + } + sort.Strings(keys) + + hourlyStats := make([]model.HourlyTokens, 0, len(keys)) + for _, k := range keys { + hourlyStats = append(hourlyStats, *bucketMap[k]) + } + + avgResponseTime := int64(0) + if responseCount > 0 { + avgResponseTime = totalResponseTime / int64(responseCount) + } + + return &model.HourlyStatsResponse{ + HourlyStats: hourlyStats, + TodayTokens: totalTokens, + TodayRequests: totalRequests, + AvgResponseTime: avgResponseTime, + }, nil +} + +// GetModelStats returns model breakdown for a specific time range +func (s *postgresStorageService) GetModelStats(startTime, endTime, orgFilter string) (*model.ModelStatsResponse, error) { + query := ` + SELECT COALESCE(model, 'unknown') as model, response + FROM requests + WHERE timestamp >= $1 AND timestamp <= $2 + ` + args := []interface{}{startTime, endTime} + if orgFilter != "" { + query += ` AND organization_id = $3` + args = append(args, orgFilter) + } + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query model stats: %w", err) + } + defer rows.Close() + + modelMap := make(map[string]*model.ModelTokens) + + for rows.Next() { + var modelName string + var responseJSON sql.NullString + + if err := rows.Scan(&modelName, &responseJSON); err != nil { + continue + } + + tokens := int64(0) + if resp, ok := decodeStoredResponse(responseJSON); ok { + if bodySummary, ok := decodeResponseBodySummary(resp.Body); ok { + tokens = totalTokensFromUsage(bodySummary.Usage) + } + } + + addModelTokens(modelMap, modelName, tokens) + } + + modelStats := make([]model.ModelTokens, 0) + for _, v := range modelMap { + modelStats = append(modelStats, *v) + } + + return &model.ModelStatsResponse{ + ModelStats: modelStats, + }, nil +} + +// GetLatestRequestDate returns the timestamp of the most recent request +func (s *postgresStorageService) GetLatestRequestDate() (*time.Time, error) { + var timestamp sql.NullTime + err := s.db.QueryRow("SELECT timestamp FROM requests ORDER BY timestamp DESC LIMIT 1").Scan(×tamp) + if err == sql.ErrNoRows || !timestamp.Valid { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to query latest request: %w", err) + } + + t := timestamp.Time + return &t, nil +} + +func (s *postgresStorageService) GetSettings() (*model.ProxySettings, error) { + var value string + err := s.db.QueryRow("SELECT value FROM settings WHERE key = 'proxy_settings'").Scan(&value) + if err == sql.ErrNoRows { + return &model.ProxySettings{}, nil + } + if err != nil { + return nil, fmt.Errorf("failed to get settings: %w", err) + } + var settings model.ProxySettings + if err := json.Unmarshal([]byte(value), &settings); err != nil { + return nil, fmt.Errorf("failed to parse settings: %w", err) + } + return &settings, nil +} + +func (s *postgresStorageService) SaveSettings(settings *model.ProxySettings) error { + data, err := json.Marshal(settings) + if err != nil { + return fmt.Errorf("failed to marshal settings: %w", err) + } + _, err = s.db.Exec( + "INSERT INTO settings (key, value) VALUES ('proxy_settings', $1) ON CONFLICT (key) DO UPDATE SET value = $1", + string(data), + ) + if err != nil { + return fmt.Errorf("failed to save settings: %w", err) + } + return nil +} + +func (s *postgresStorageService) GetDistinctOrganizations() ([]string, error) { + rows, err := s.db.Query(`SELECT DISTINCT organization_id FROM requests WHERE organization_id IS NOT NULL AND organization_id != '' ORDER BY organization_id`) + if err != nil { + return nil, fmt.Errorf("failed to query organizations: %w", err) + } + defer rows.Close() + + var orgs []string + for rows.Next() { + var org string + if err := rows.Scan(&org); err != nil { + continue + } + orgs = append(orgs, org) + } + return orgs, nil +} diff --git a/proxy/internal/service/storage_postgres_contract_test.go b/proxy/internal/service/storage_postgres_contract_test.go new file mode 100644 index 0000000..6e58d58 --- /dev/null +++ b/proxy/internal/service/storage_postgres_contract_test.go @@ -0,0 +1,60 @@ +package service + +import ( + "os" + "testing" + + "github.com/seifghazi/claude-code-monitor/internal/config" +) + +func TestPostgresStorageContract(t *testing.T) { + dsn := os.Getenv("TEST_POSTGRES_DSN") + if dsn == "" { + t.Skip("TEST_POSTGRES_DSN not set") + } + + runStorageContractTests(t, storageFactory{ + name: "postgres", + new: func(t *testing.T, cfg config.StorageConfig) StorageService { + t.Helper() + cfg.DBType = "postgres" + cfg.DatabaseURL = dsn + return newTestPostgresStorage(t, cfg) + }, + }) +} + +func newTestPostgresStorage(t *testing.T, cfg config.StorageConfig) *postgresStorageService { + t.Helper() + + storage, err := NewPostgresStorageService(&cfg) + if err != nil { + t.Fatalf("NewPostgresStorageService() error = %v", err) + } + + pgStorage, ok := storage.(*postgresStorageService) + if !ok { + t.Fatalf("unexpected storage type %T", storage) + } + + resetPostgresTestStorage(t, pgStorage) + t.Cleanup(func() { + resetPostgresTestStorage(t, pgStorage) + if err := pgStorage.Close(); err != nil { + t.Errorf("Close() error = %v", err) + } + }) + + return pgStorage +} + +func resetPostgresTestStorage(t *testing.T, storage *postgresStorageService) { + t.Helper() + + if _, err := storage.db.Exec("TRUNCATE TABLE requests"); err != nil { + t.Fatalf("TRUNCATE requests error = %v", err) + } + if _, err := storage.db.Exec("DELETE FROM settings"); err != nil { + t.Fatalf("DELETE settings error = %v", err) + } +} diff --git a/proxy/internal/service/storage_query_helpers.go b/proxy/internal/service/storage_query_helpers.go new file mode 100644 index 0000000..eec060c --- /dev/null +++ b/proxy/internal/service/storage_query_helpers.go @@ -0,0 +1,38 @@ +package service + +import ( + "strings" + + "github.com/seifghazi/claude-code-monitor/internal/model" +) + +func modelFilterPattern(modelFilter string, escaper func(string) string) (string, bool) { + normalized := strings.TrimSpace(strings.ToLower(modelFilter)) + if normalized == "" || normalized == "all" { + return "", false + } + + return "%" + escaper(normalized) + "%", true +} + +func addUsageStats(stats *model.UsageStats, modelName string, usage *model.AnthropicUsage) { + if stats == nil || usage == nil { + return + } + + inputTokens := int64(usage.InputTokens) + outputTokens := int64(usage.OutputTokens) + cacheTokens := int64(usage.CacheCreationInputTokens + usage.CacheReadInputTokens) + + stats.TotalRequests++ + stats.TotalInputTokens += inputTokens + stats.TotalOutputTokens += outputTokens + stats.TotalCacheTokens += cacheTokens + + modelStats := stats.RequestsByModel[modelName] + modelStats.RequestCount++ + modelStats.InputTokens += inputTokens + modelStats.OutputTokens += outputTokens + modelStats.CacheTokens += cacheTokens + stats.RequestsByModel[modelName] = modelStats +} diff --git a/proxy/internal/service/storage_sqlite.go b/proxy/internal/service/storage_sqlite.go index a9e00db..8dbbd82 100644 --- a/proxy/internal/service/storage_sqlite.go +++ b/proxy/internal/service/storage_sqlite.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "sort" "strings" "time" @@ -115,35 +116,44 @@ func (s *sqliteStorageService) createTables() error { } // Run migrations - s.migrateSchema() + if err := s.migrateSchema(); err != nil { + return err + } return nil } -func (s *sqliteStorageService) migrateSchema() { +func (s *sqliteStorageService) migrateSchema() error { // Ensure WAL mode is enabled (in case opened without connection string params) _, err := s.db.Exec("PRAGMA journal_mode=WAL") if err != nil { - s.logger.Printf("Warning: failed to set WAL mode: %v", err) + return fmt.Errorf("failed to set WAL mode: %w", err) } - // Drop old redundant index if it exists (we renamed to idx_requests_timestamp) - s.db.Exec("DROP INDEX IF EXISTS idx_timestamp") + return runMigrations(s.db, []string{ + "DROP INDEX IF EXISTS idx_timestamp", + "ALTER TABLE requests ADD COLUMN conversation_hash TEXT", + "ALTER TABLE requests ADD COLUMN message_count INTEGER DEFAULT 0", + "CREATE INDEX IF NOT EXISTS idx_requests_conversation_hash ON requests(conversation_hash)", + "ALTER TABLE requests ADD COLUMN organization_id TEXT", + "CREATE INDEX IF NOT EXISTS idx_requests_organization_id ON requests(organization_id)", + `CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)`, + }, ignoreSQLiteDuplicateColumn) } func (s *sqliteStorageService) prepareStatements() error { var err error s.stmtInsertRequest, err = s.db.Prepare(` - INSERT INTO requests (id, timestamp, method, endpoint, headers, body, user_agent, content_type, model, original_model, routed_model) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO requests (id, timestamp, method, endpoint, headers, body, user_agent, content_type, model, original_model, routed_model, conversation_hash, message_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `) if err != nil { return fmt.Errorf("failed to prepare insert statement: %w", err) } s.stmtUpdateResponse, err = s.db.Prepare(` - UPDATE requests SET response = ? WHERE id = ? + UPDATE requests SET response = ?, organization_id = COALESCE(NULLIF(?, ''), organization_id) WHERE id = ? `) if err != nil { return fmt.Errorf("failed to prepare update response statement: %w", err) @@ -198,7 +208,7 @@ func (s *sqliteStorageService) SaveRequest(request *model.RequestLog) (string, e return "", fmt.Errorf("failed to marshal headers: %w", err) } - bodyForStorage, err := s.prepareRequestBodyForStorage(request.Body) + bodyForStorage, err := prepareRequestBodyForStorage(s.config, request.Body) if err != nil { return "", fmt.Errorf("failed to prepare body for storage: %w", err) } @@ -220,6 +230,8 @@ func (s *sqliteStorageService) SaveRequest(request *model.RequestLog) (string, e request.Model, request.OriginalModel, request.RoutedModel, + request.ConversationHash, + request.MessageCount, ) if err != nil { @@ -238,11 +250,8 @@ func (s *sqliteStorageService) GetRequests(page, limit int, modelFilter string) countArgs := []interface{}{} queryArgs := []interface{}{} - if modelFilter != "" && modelFilter != "all" { - // Escape LIKE special characters to prevent pattern injection - escapedFilter := escapeLikePattern(strings.ToLower(modelFilter)) + if filterValue, ok := modelFilterPattern(modelFilter, escapeLikePattern); ok { whereClause = " WHERE LOWER(model) LIKE ? ESCAPE '\\'" - filterValue := "%" + escapedFilter + "%" countArgs = append(countArgs, filterValue) queryArgs = append(queryArgs, filterValue) } @@ -323,7 +332,7 @@ func (s *sqliteStorageService) UpdateRequestWithGrading(requestID string, grade } func (s *sqliteStorageService) UpdateRequestWithResponse(request *model.RequestLog) error { - responseForStorage, err := s.prepareResponseForStorage(request.Response) + responseForStorage, err := prepareResponseForStorage(s.config, s.logger, request.Response) if err != nil { return fmt.Errorf("failed to prepare response for storage: %w", err) } @@ -333,7 +342,8 @@ func (s *sqliteStorageService) UpdateRequestWithResponse(request *model.RequestL return fmt.Errorf("failed to marshal response: %w", err) } - result, err := s.stmtUpdateResponse.Exec(string(responseJSON), request.RequestID) + orgID := request.OrganizationID + result, err := s.stmtUpdateResponse.Exec(string(responseJSON), orgID, request.RequestID) if err != nil { return fmt.Errorf("failed to update request with response: %w", err) } @@ -359,7 +369,7 @@ func (s *sqliteStorageService) SaveRequestWithResponse(request *model.RequestLog return fmt.Errorf("failed to marshal headers: %w", err) } - bodyForStorage, err := s.prepareRequestBodyForStorage(request.Body) + bodyForStorage, err := prepareRequestBodyForStorage(s.config, request.Body) if err != nil { return fmt.Errorf("failed to prepare body for storage: %w", err) } @@ -382,6 +392,8 @@ func (s *sqliteStorageService) SaveRequestWithResponse(request *model.RequestLog request.Model, request.OriginalModel, request.RoutedModel, + request.ConversationHash, + request.MessageCount, ) if err != nil { return fmt.Errorf("failed to insert request: %w", err) @@ -389,7 +401,7 @@ func (s *sqliteStorageService) SaveRequestWithResponse(request *model.RequestLog // Update with response if present if request.Response != nil { - responseForStorage, err := s.prepareResponseForStorage(request.Response) + responseForStorage, err := prepareResponseForStorage(s.config, s.logger, request.Response) if err != nil { return fmt.Errorf("failed to prepare response for storage: %w", err) } @@ -460,7 +472,7 @@ func (s *sqliteStorageService) GetRequestByShortID(shortID string) (*model.Reque return nil, "", fmt.Errorf("failed to query request: %w", err) } - if err := s.unmarshalRequestFields(&req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil { + if err := unmarshalStoredRequestFields(s.logger, &req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil { return nil, "", err } @@ -480,16 +492,14 @@ func (s *sqliteStorageService) GetAllRequestsWithLimit(modelFilter string, limit var query string args := []interface{}{} - if modelFilter != "" && modelFilter != "all" { - // Escape LIKE special characters - escapedFilter := escapeLikePattern(strings.ToLower(modelFilter)) + if filterValue, ok := modelFilterPattern(modelFilter, escapeLikePattern); ok { query = ` SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model FROM requests WHERE LOWER(model) LIKE ? ESCAPE '\' ORDER BY timestamp DESC ` - args = append(args, "%"+escapedFilter+"%") + args = append(args, filterValue) } else { query = ` SELECT id, timestamp, method, endpoint, headers, body, model, user_agent, content_type, prompt_grade, response, original_model, routed_model @@ -615,8 +625,6 @@ func (s *sqliteStorageService) Close() error { // Helper functions -const redactionPlaceholder = "[REDACTED]" - // escapeLikePattern escapes special characters in LIKE patterns func escapeLikePattern(s string) string { // Escape \, %, and _ characters @@ -655,7 +663,7 @@ func (s *sqliteStorageService) scanRequestRows(rows *sql.Rows) ([]model.RequestL continue } - if err := s.unmarshalRequestFields(&req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil { + if err := unmarshalStoredRequestFields(s.logger, &req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil { s.logger.Printf("Warning: failed to unmarshal request fields: %v", err) continue } @@ -695,46 +703,13 @@ func (s *sqliteStorageService) scanSingleRow(rows *sql.Rows) (*model.RequestLog, return nil, fmt.Errorf("failed to scan row: %w", err) } - if err := s.unmarshalRequestFields(&req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil { + if err := unmarshalStoredRequestFields(s.logger, &req, headersJSON, bodyJSON, promptGradeJSON, responseJSON); err != nil { return nil, err } return &req, nil } -// unmarshalRequestFields unmarshals JSON fields into a RequestLog -func (s *sqliteStorageService) unmarshalRequestFields(req *model.RequestLog, headersJSON, bodyJSON string, promptGradeJSON, responseJSON sql.NullString) error { - if err := json.Unmarshal([]byte(headersJSON), &req.Headers); err != nil { - return fmt.Errorf("failed to unmarshal headers: %w", err) - } - - var body interface{} - if err := json.Unmarshal([]byte(bodyJSON), &body); err != nil { - return fmt.Errorf("failed to unmarshal body: %w", err) - } - req.Body = body - - if promptGradeJSON.Valid { - var grade model.PromptGrade - if err := json.Unmarshal([]byte(promptGradeJSON.String), &grade); err != nil { - s.logger.Printf("Warning: failed to unmarshal prompt grade: %v", err) - } else { - req.PromptGrade = &grade - } - } - - if responseJSON.Valid { - var resp model.ResponseLog - if err := json.Unmarshal([]byte(responseJSON.String), &resp); err != nil { - s.logger.Printf("Warning: failed to unmarshal response: %v", err) - } else { - req.Response = &resp - } - } - - return nil -} - func (s *sqliteStorageService) cleanupExpiredRequests() error { if s.config == nil || s.config.RetentionDays <= 0 { return nil @@ -744,136 +719,498 @@ func (s *sqliteStorageService) cleanupExpiredRequests() error { return err } -func (s *sqliteStorageService) prepareRequestBodyForStorage(body interface{}) (interface{}, error) { - if s.shouldSuppressBodies() { - return storageBodyPlaceholder("metadata_only"), nil - } - if s.config != nil && !s.config.CaptureRequestBody { - return storageBodyPlaceholder("request_body_disabled"), nil +// GetUsageStats returns aggregated token usage statistics +func (s *sqliteStorageService) GetUsageStats(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) { + stats := &model.UsageStats{ + RequestsByModel: make(map[string]model.ModelStats), } - normalized, err := normalizeJSONValue(body) + // Build query with optional filters + whereClause := "WHERE response IS NOT NULL" + args := []interface{}{} + + if startDate != "" { + whereClause += " AND timestamp >= ?" + args = append(args, startDate) + stats.StartDate = startDate + } + + if endDate != "" { + whereClause += " AND timestamp <= ?" + args = append(args, endDate) + stats.EndDate = endDate + } + + if filterValue, ok := modelFilterPattern(modelFilter, escapeLikePattern); ok { + whereClause += " AND LOWER(model) LIKE ? ESCAPE '\\'" + args = append(args, filterValue) + } + + if orgFilter != "" { + whereClause += " AND organization_id = ?" + args = append(args, orgFilter) + } + + // Query all responses and aggregate token usage + query := ` + SELECT model, response + FROM requests + ` + whereClause + + rows, err := s.db.Query(query, args...) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to query usage stats: %w", err) } + defer rows.Close() - fields := []string{} - if s.config != nil { - fields = s.config.RedactedFields - } + for rows.Next() { + var modelName string + var responseJSON sql.NullString - return redactJSONValue(normalized, redactedFieldSet(fields)), nil -} - -func (s *sqliteStorageService) prepareResponseForStorage(response *model.ResponseLog) (*model.ResponseLog, error) { - if response == nil { - return nil, nil - } - - clone := *response - if s.shouldSuppressBodies() || (s.config != nil && !s.config.CaptureResponseBody) { - clone.Body = nil - clone.BodyText = "" - clone.StreamingChunks = nil - return &clone, nil - } - - if len(clone.Body) > 0 { - fields := []string{} - if s.config != nil { - fields = s.config.RedactedFields - } - - sanitizedBody, err := sanitizeRawJSON(clone.Body, redactedFieldSet(fields)) - if err != nil { - // Preserve the original payload if it cannot be parsed as JSON. - s.logger.Printf("Warning: failed to redact response body: %v", err) - } else { - clone.Body = sanitizedBody - } - } - - return &clone, nil -} - -func (s *sqliteStorageService) shouldSuppressBodies() bool { - return s.config != nil && s.config.MetadataOnly -} - -func normalizeJSONValue(value interface{}) (interface{}, error) { - if value == nil { - return nil, nil - } - - data, err := json.Marshal(value) - if err != nil { - return nil, err - } - - var normalized interface{} - if err := json.Unmarshal(data, &normalized); err != nil { - return nil, err - } - - return normalized, nil -} - -func sanitizeRawJSON(raw json.RawMessage, redacted map[string]struct{}) (json.RawMessage, error) { - if len(raw) == 0 { - return raw, nil - } - - var value interface{} - if err := json.Unmarshal(raw, &value); err != nil { - return raw, err - } - - sanitized := redactJSONValue(value, redacted) - data, err := json.Marshal(sanitized) - if err != nil { - return raw, err - } - - return json.RawMessage(data), nil -} - -func redactJSONValue(value interface{}, redacted map[string]struct{}) interface{} { - switch typed := value.(type) { - case map[string]interface{}: - result := make(map[string]interface{}, len(typed)) - for key, child := range typed { - if _, ok := redacted[strings.ToLower(key)]; ok { - result[key] = redactionPlaceholder - continue - } - result[key] = redactJSONValue(child, redacted) - } - return result - case []interface{}: - result := make([]interface{}, len(typed)) - for i, child := range typed { - result[i] = redactJSONValue(child, redacted) - } - return result - default: - return value - } -} - -func storageBodyPlaceholder(mode string) map[string]interface{} { - return map[string]interface{}{ - "_storage_mode": mode, - } -} - -func redactedFieldSet(fields []string) map[string]struct{} { - set := make(map[string]struct{}, len(fields)) - for _, field := range fields { - field = strings.TrimSpace(strings.ToLower(field)) - if field == "" { + if err := rows.Scan(&modelName, &responseJSON); err != nil { + s.logger.Printf("Warning: failed to scan usage row: %v", err) continue } - set[field] = struct{}{} + + resp, ok := decodeStoredResponse(responseJSON) + if !ok { + continue + } + bodySummary, ok := decodeResponseBodySummary(resp.Body) + if !ok || bodySummary.Usage == nil { + continue + } + + addUsageStats(stats, modelName, bodySummary.Usage) } - return set + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating usage rows: %w", err) + } + + // Get date range if not specified + if stats.StartDate == "" || stats.EndDate == "" { + var oldest, newest sql.NullString + err := s.db.QueryRow("SELECT MIN(timestamp), MAX(timestamp) FROM requests WHERE response IS NOT NULL").Scan(&oldest, &newest) + if err == nil { + if stats.StartDate == "" && oldest.Valid { + stats.StartDate = oldest.String + } + if stats.EndDate == "" && newest.Valid { + stats.EndDate = newest.String + } + } + } + + return stats, nil +} + +// GetRequestsSummary returns minimal data for list view - no body/headers, only usage from response +func (s *sqliteStorageService) GetRequestsSummary(modelFilter string) ([]*model.RequestSummary, error) { + query := ` + SELECT id, timestamp, method, endpoint, model, original_model, routed_model, response, COALESCE(conversation_hash, ''), COALESCE(message_count, 0) + FROM requests + ` + args := []interface{}{} + + if filterValue, ok := modelFilterPattern(modelFilter, escapeLikePattern); ok { + query += " WHERE LOWER(model) LIKE ? ESCAPE '\\'" + args = append(args, filterValue) + } + + query += " ORDER BY timestamp DESC" + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query requests: %w", err) + } + defer rows.Close() + + var summaries []*model.RequestSummary + for rows.Next() { + var summary model.RequestSummary + var responseJSON sql.NullString + + err := rows.Scan( + &summary.RequestID, + &summary.Timestamp, + &summary.Method, + &summary.Endpoint, + &summary.Model, + &summary.OriginalModel, + &summary.RoutedModel, + &responseJSON, + &summary.ConversationHash, + &summary.MessageCount, + ) + if err != nil { + s.logger.Printf("Warning: failed to scan summary row: %v", err) + continue + } + + // Only parse response to extract usage and status + applyStoredResponseToSummary(&summary, responseJSON) + + summaries = append(summaries, &summary) + } + + return summaries, nil +} + +// GetRequestsSummaryPaginated returns minimal data for list view with pagination +func (s *sqliteStorageService) GetRequestsSummaryPaginated(modelFilter, startTime, endTime string, offset, limit int) ([]*model.RequestSummary, int, error) { + // Build WHERE clauses + whereClauses := []string{} + args := []interface{}{} + + if filterValue, ok := modelFilterPattern(modelFilter, escapeLikePattern); ok { + whereClauses = append(whereClauses, "LOWER(model) LIKE ? ESCAPE '\\'") + args = append(args, filterValue) + } + + if startTime != "" && endTime != "" { + whereClauses = append(whereClauses, "datetime(timestamp) >= datetime(?) AND datetime(timestamp) <= datetime(?)") + args = append(args, startTime, endTime) + } + + whereClause := "" + if len(whereClauses) > 0 { + whereClause = " WHERE " + strings.Join(whereClauses, " AND ") + } + + // Get total count + var total int + countQuery := "SELECT COUNT(*) FROM requests" + whereClause + countArgs := make([]interface{}, len(args)) + copy(countArgs, args) + if err := s.db.QueryRow(countQuery, countArgs...).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("failed to get total count: %w", err) + } + + // Get the requested page + query := ` + SELECT id, timestamp, method, endpoint, model, original_model, routed_model, response, COALESCE(conversation_hash, ''), COALESCE(message_count, 0) + FROM requests + ` + whereClause + " ORDER BY timestamp DESC" + + // Add pagination + if limit > 0 { + query += " LIMIT ? OFFSET ?" + args = append(args, limit, offset) + } else if offset > 0 { + query += " OFFSET ?" + args = append(args, offset) + } + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, 0, fmt.Errorf("failed to query requests: %w", err) + } + defer rows.Close() + + var summaries []*model.RequestSummary + for rows.Next() { + var summary model.RequestSummary + var responseJSON sql.NullString + + err := rows.Scan( + &summary.RequestID, + &summary.Timestamp, + &summary.Method, + &summary.Endpoint, + &summary.Model, + &summary.OriginalModel, + &summary.RoutedModel, + &responseJSON, + &summary.ConversationHash, + &summary.MessageCount, + ) + if err != nil { + s.logger.Printf("Warning: failed to scan summary row: %v", err) + continue + } + + // Only parse response to extract usage and status + applyStoredResponseToSummary(&summary, responseJSON) + + summaries = append(summaries, &summary) + } + + s.logger.Printf("📊 GetRequestsSummaryPaginated: returned %d requests (total: %d, limit: %d, offset: %d)", len(summaries), total, limit, offset) + return summaries, total, nil +} + +// GetStats returns aggregated statistics for the dashboard - daily token usage +func (s *sqliteStorageService) GetStats(startDate, endDate, orgFilter string) (*model.DashboardStats, error) { + stats := &model.DashboardStats{ + DailyStats: make([]model.DailyTokens, 0), + } + + query := ` + SELECT timestamp, COALESCE(model, 'unknown') as model, response + FROM requests + WHERE datetime(timestamp) >= datetime(?) AND datetime(timestamp) <= datetime(?) + ` + args := []interface{}{startDate, endDate} + if orgFilter != "" { + query += ` AND organization_id = ?` + args = append(args, orgFilter) + } + query += ` ORDER BY timestamp` + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query stats: %w", err) + } + defer rows.Close() + + // Aggregate data in memory + dailyMap := make(map[string]*model.DailyTokens) + + for rows.Next() { + var timestamp, modelName string + var responseJSON sql.NullString + + if err := rows.Scan(×tamp, &modelName, &responseJSON); err != nil { + continue + } + + // Extract date from timestamp (format: 2025-11-28T13:03:29-08:00) + date := strings.Split(timestamp, "T")[0] + + // Parse response to get usage + tokens := int64(0) + if resp, ok := decodeStoredResponse(responseJSON); ok { + if bodySummary, ok := decodeResponseBodySummary(resp.Body); ok { + tokens = totalTokensFromUsage(bodySummary.Usage) + } + } + + addDailyTokens(dailyMap, date, modelName, tokens) + } + + // Convert map to slice + for _, v := range dailyMap { + stats.DailyStats = append(stats.DailyStats, *v) + } + + return stats, nil +} + +// GetHourlyStats returns time-bucketed breakdown for a specific time range. +// bucketMinutes controls the granularity (e.g. 5, 15, 30, 60). +func (s *sqliteStorageService) GetHourlyStats(startTime, endTime string, bucketMinutes int, orgFilter string) (*model.HourlyStatsResponse, error) { + if bucketMinutes <= 0 { + bucketMinutes = 60 + } + + query := ` + SELECT timestamp, COALESCE(model, 'unknown') as model, response + FROM requests + WHERE datetime(timestamp) >= datetime(?) AND datetime(timestamp) <= datetime(?) + ` + args := []interface{}{startTime, endTime} + if orgFilter != "" { + query += ` AND organization_id = ?` + args = append(args, orgFilter) + } + query += ` ORDER BY timestamp` + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query hourly stats: %w", err) + } + defer rows.Close() + + bucketMap := make(map[string]*model.HourlyTokens) + var totalTokens int64 + var totalRequests int + var totalResponseTime int64 + var responseCount int + + for rows.Next() { + var timestamp, modelName string + var responseJSON sql.NullString + + if err := rows.Scan(×tamp, &modelName, &responseJSON); err != nil { + continue + } + + // Compute bucket key from timestamp + bucketKey := "" + bucketLabel := "" + if t, err := time.Parse(time.RFC3339, timestamp); err == nil { + // Always use absolute time buckets so multi-day ranges show per-slot data + minuteOfDay := t.Hour()*60 + t.Minute() + bucketStart := (minuteOfDay / bucketMinutes) * bucketMinutes + bucketTime := time.Date(t.Year(), t.Month(), t.Day(), bucketStart/60, bucketStart%60, 0, 0, t.Location()) + bucketKey = bucketTime.Format("2006-01-02T15:04") + bucketLabel = bucketTime.Format("Jan 2 15:04") + } + + // Parse response to get usage and response time + tokens := int64(0) + responseTime := int64(0) + if resp, ok := decodeStoredResponse(responseJSON); ok { + responseTime = resp.ResponseTime + if bodySummary, ok := decodeResponseBodySummary(resp.Body); ok { + tokens = totalTokensFromUsage(bodySummary.Usage) + } + } + + totalTokens += tokens + totalRequests++ + + // Track response time + if responseTime > 0 { + totalResponseTime += responseTime + responseCount++ + } + + addHourlyTokens(bucketMap, bucketKey, bucketLabel, modelName, tokens) + } + + // Convert map to sorted slice + keys := make([]string, 0, len(bucketMap)) + for k := range bucketMap { + keys = append(keys, k) + } + sort.Strings(keys) + + hourlyStats := make([]model.HourlyTokens, 0, len(keys)) + for _, k := range keys { + hourlyStats = append(hourlyStats, *bucketMap[k]) + } + + // Calculate average response time + avgResponseTime := int64(0) + if responseCount > 0 { + avgResponseTime = totalResponseTime / int64(responseCount) + } + + return &model.HourlyStatsResponse{ + HourlyStats: hourlyStats, + TodayTokens: totalTokens, + TodayRequests: totalRequests, + AvgResponseTime: avgResponseTime, + }, nil +} + +// GetModelStats returns model breakdown for a specific time range +func (s *sqliteStorageService) GetModelStats(startTime, endTime, orgFilter string) (*model.ModelStatsResponse, error) { + query := ` + SELECT COALESCE(model, 'unknown') as model, response + FROM requests + WHERE datetime(timestamp) >= datetime(?) AND datetime(timestamp) <= datetime(?) + ` + args := []interface{}{startTime, endTime} + if orgFilter != "" { + query += ` AND organization_id = ?` + args = append(args, orgFilter) + } + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("failed to query model stats: %w", err) + } + defer rows.Close() + + modelMap := make(map[string]*model.ModelTokens) + + for rows.Next() { + var modelName string + var responseJSON sql.NullString + + if err := rows.Scan(&modelName, &responseJSON); err != nil { + continue + } + + // Parse response to get usage + tokens := int64(0) + if resp, ok := decodeStoredResponse(responseJSON); ok { + if bodySummary, ok := decodeResponseBodySummary(resp.Body); ok { + tokens = totalTokensFromUsage(bodySummary.Usage) + } + } + + addModelTokens(modelMap, modelName, tokens) + } + + // Convert map to slice + modelStats := make([]model.ModelTokens, 0) + for _, v := range modelMap { + modelStats = append(modelStats, *v) + } + + return &model.ModelStatsResponse{ + ModelStats: modelStats, + }, nil +} + +// GetLatestRequestDate returns the timestamp of the most recent request +func (s *sqliteStorageService) GetLatestRequestDate() (*time.Time, error) { + var timestamp string + err := s.db.QueryRow("SELECT timestamp FROM requests ORDER BY timestamp DESC LIMIT 1").Scan(×tamp) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("failed to query latest request: %w", err) + } + + t, err := time.Parse(time.RFC3339, timestamp) + if err != nil { + return nil, fmt.Errorf("failed to parse timestamp: %w", err) + } + + return &t, nil +} + +func (s *sqliteStorageService) GetSettings() (*model.ProxySettings, error) { + var value string + err := s.db.QueryRow("SELECT value FROM settings WHERE key = 'proxy_settings'").Scan(&value) + if err == sql.ErrNoRows { + return &model.ProxySettings{}, nil + } + if err != nil { + return nil, fmt.Errorf("failed to get settings: %w", err) + } + var settings model.ProxySettings + if err := json.Unmarshal([]byte(value), &settings); err != nil { + return nil, fmt.Errorf("failed to parse settings: %w", err) + } + return &settings, nil +} + +func (s *sqliteStorageService) SaveSettings(settings *model.ProxySettings) error { + data, err := json.Marshal(settings) + if err != nil { + return fmt.Errorf("failed to marshal settings: %w", err) + } + _, err = s.db.Exec( + "INSERT OR REPLACE INTO settings (key, value) VALUES ('proxy_settings', ?)", + string(data), + ) + if err != nil { + return fmt.Errorf("failed to save settings: %w", err) + } + return nil +} + +func (s *sqliteStorageService) GetDistinctOrganizations() ([]string, error) { + rows, err := s.db.Query(`SELECT DISTINCT organization_id FROM requests WHERE organization_id IS NOT NULL AND organization_id != '' ORDER BY organization_id`) + if err != nil { + return nil, fmt.Errorf("failed to query organizations: %w", err) + } + defer rows.Close() + + var orgs []string + for rows.Next() { + var org string + if err := rows.Scan(&org); err != nil { + continue + } + orgs = append(orgs, org) + } + return orgs, nil } diff --git a/proxy/internal/service/storage_sqlite_contract_test.go b/proxy/internal/service/storage_sqlite_contract_test.go new file mode 100644 index 0000000..37bcea3 --- /dev/null +++ b/proxy/internal/service/storage_sqlite_contract_test.go @@ -0,0 +1,19 @@ +package service + +import ( + "testing" + + "github.com/seifghazi/claude-code-monitor/internal/config" +) + +func TestSQLiteStorageContract(t *testing.T) { + runStorageContractTests(t, storageFactory{ + name: "sqlite", + new: newTestSQLiteStorageService, + }) +} + +func newTestSQLiteStorageService(t *testing.T, cfg config.StorageConfig) StorageService { + t.Helper() + return newTestSQLiteStorage(t, cfg) +} diff --git a/run.sh b/run.sh index a6b09e1..b88a5a3 100755 --- a/run.sh +++ b/run.sh @@ -1,10 +1,10 @@ #!/bin/bash -# Claude Code Monitor - Build and Run Script +# Claude Code Proxy - Build and Run Script set -e -echo "🚀 Claude Code Monitor - Starting Services" +echo "🚀 Claude Code Proxy - Starting Services" echo "=========================================" # Colors for output @@ -40,7 +40,7 @@ fi # Function to cleanup on exit cleanup() { echo -e "\n${YELLOW}Shutting down services...${NC}" - kill $PROXY_PID $WEB_PID 2>/dev/null || true + kill $PROXY_PID $SVELTE_PID 2>/dev/null || true exit } @@ -55,13 +55,13 @@ cd .. echo -e "${GREEN}✅ Proxy server built${NC}" -# Install web dependencies if needed -if [ ! -d "web/node_modules" ]; then - echo -e "\n${BLUE}📦 Installing web dependencies...${NC}" - cd web +# Install svelte dependencies if needed +if [ ! -d "svelte/node_modules" ]; then + echo -e "\n${BLUE}📦 Installing svelte dependencies...${NC}" + cd svelte npm install cd .. - echo -e "${GREEN}✅ Web dependencies installed${NC}" + echo -e "${GREEN}✅ Svelte dependencies installed${NC}" fi # Start proxy server @@ -72,20 +72,20 @@ PROXY_PID=$! # Wait for proxy to start sleep 2 -# Start web server -echo -e "${BLUE}🚀 Starting web interface on port 5173...${NC}" -cd web +# Start svelte server +echo -e "${BLUE}🚀 Starting Svelte Dashboard on port 5174...${NC}" +cd svelte npm run dev & -WEB_PID=$! +SVELTE_PID=$! cd .. echo -e "\n${GREEN}✨ All services started!${NC}" echo "=========================================" -echo -e "📊 Web Dashboard: ${BLUE}http://localhost:5173${NC}" +echo -e "📊 Svelte Dashboard: ${BLUE}http://localhost:5174${NC}" echo -e "🔌 API Proxy: ${BLUE}http://localhost:3001${NC}" echo -e "💚 Health Check: ${BLUE}http://localhost:3001/health${NC}" echo "=========================================" echo -e "${YELLOW}Press Ctrl+C to stop all services${NC}\n" # Wait for processes -wait \ No newline at end of file +wait diff --git a/shared/frontend/backend.ts b/shared/frontend/backend.ts new file mode 100644 index 0000000..90a3738 --- /dev/null +++ b/shared/frontend/backend.ts @@ -0,0 +1,13 @@ +export const DEFAULT_BACKEND_ORIGIN = 'http://localhost:3001'; + +export function resolveBackendOrigin(env: Record): string { + return (env.BACKEND_URL || env.PROXY_BACKEND_URL || DEFAULT_BACKEND_ORIGIN).replace(/\/$/, ''); +} + +export function buildBackendURL(origin: string, path: string, searchParams?: URLSearchParams): string { + const url = new URL(path, `${origin}/`); + if (searchParams) { + url.search = searchParams.toString(); + } + return url.toString(); +} diff --git a/shared/frontend/formatters.ts b/shared/frontend/formatters.ts new file mode 100644 index 0000000..6a361d4 --- /dev/null +++ b/shared/frontend/formatters.ts @@ -0,0 +1,382 @@ +import type { MessageContent, TextContentBlock } from './types'; + +/** + * Utility functions for formatting and displaying data + */ + +/** + * Safely converts unknown values to a formatted string for display + */ +export function formatValue(value: unknown): string { + if (value === null) return 'null'; + if (value === undefined) return 'undefined'; + if (typeof value === 'string') return value; + if (typeof value === 'number' || typeof value === 'boolean') return String(value); + + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +/** + * Formats JSON with proper indentation and returns a formatted string + * Set maxLength to 0 or Infinity for no truncation + */ +export function formatJSON(obj: unknown, maxLength: number = 50000): string { + try { + const jsonString = JSON.stringify(obj, null, 2); + if (maxLength > 0 && maxLength < Infinity && jsonString.length > maxLength) { + return jsonString.substring(0, maxLength) + '\n... (truncated - ' + jsonString.length.toLocaleString() + ' total chars)'; + } + return jsonString; + } catch { + return String(obj); + } +} + +/** + * Formats JSON without truncation + */ +export function formatJSONFull(obj: unknown): string { + try { + return JSON.stringify(obj, null, 2); + } catch { + return String(obj); + } +} + +/** + * Escapes HTML characters to prevent XSS. + */ +export function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * Formats large text with proper line breaks and structure. + * Supports markdown-like syntax: headings, bold, italic, inline code, + * fenced code blocks, bullet/numbered lists, horizontal rules, and URLs. + */ +export function formatLargeText(text: string): string { + if (!text) return ''; + + const codeBlocks: string[] = []; + const withPlaceholders = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, _lang, code) => { + const idx = codeBlocks.length; + codeBlocks.push( + `
${escapeHtml(code.replace(/\n$/, ''))}
` + ); + return `\x00CODEBLOCK_${idx}\x00`; + }); + + const escaped = escapeHtml(withPlaceholders); + const lines = escaped.split('\n'); + const outputLines: string[] = []; + let inList = false; + let listType: 'ul' | 'ol' = 'ul'; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + const cbMatch = line.match(/\x00CODEBLOCK_(\d+)\x00/); + if (cbMatch) { + if (inList) { + outputLines.push(listType === 'ul' ? '' : ''); + inList = false; + } + outputLines.push(codeBlocks[parseInt(cbMatch[1], 10)]); + continue; + } + + if (/^(-{3,}|\*{3,}|_{3,})$/.test(line.trim())) { + if (inList) { + outputLines.push(listType === 'ul' ? '' : ''); + inList = false; + } + outputLines.push('
'); + continue; + } + + const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); + if (headingMatch) { + if (inList) { + outputLines.push(listType === 'ul' ? '' : ''); + inList = false; + } + const level = headingMatch[1].length; + const headingText = headingMatch[2]; + const sizes: Record = { + 1: 'text-xl font-bold text-gray-900 mt-5 mb-2', + 2: 'text-lg font-bold text-gray-900 mt-4 mb-2', + 3: 'text-base font-semibold text-gray-800 mt-3 mb-1', + 4: 'text-sm font-semibold text-gray-800 mt-2 mb-1', + 5: 'text-sm font-medium text-gray-700 mt-2 mb-1', + 6: 'text-xs font-medium text-gray-700 mt-2 mb-1', + }; + outputLines.push(`
${applyInlineFormatting(headingText)}
`); + continue; + } + + const bulletMatch = line.match(/^(\s*)[-*+]\s+(.+)$/); + if (bulletMatch) { + if (!inList || listType !== 'ul') { + if (inList) outputLines.push(listType === 'ul' ? '' : ''); + outputLines.push('
    '); + inList = true; + listType = 'ul'; + } + outputLines.push(`
  • ${applyInlineFormatting(bulletMatch[2])}
  • `); + continue; + } + + const numMatch = line.match(/^(\s*)\d+[.)]\s+(.+)$/); + if (numMatch) { + if (!inList || listType !== 'ol') { + if (inList) outputLines.push(listType === 'ul' ? '
' : ''); + outputLines.push('
    '); + inList = true; + listType = 'ol'; + } + outputLines.push(`
  1. ${applyInlineFormatting(numMatch[2])}
  2. `); + continue; + } + + if (inList && line.trim() !== '') { + outputLines.push(listType === 'ul' ? '' : '
'); + inList = false; + } + + if (line.trim() === '') { + outputLines.push('
'); + continue; + } + + outputLines.push(`
${applyInlineFormatting(line)}
`); + } + + if (inList) outputLines.push(listType === 'ul' ? '' : ''); + + return outputLines.join('\n'); +} + +function applyInlineFormatting(text: string): string { + return text + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/\bhttps?:\/\/[^\s<&]+/g, (url) => { + return `${url}`; + }); +} + +export function isComplexObject(value: unknown): boolean { + return value !== null && + typeof value === 'object' && + !Array.isArray(value) && + Object.keys(value).length > 0; +} + +export function truncateText(text: string, maxLength: number = 2000): string { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength) + '... (' + (text.length - maxLength).toLocaleString() + ' more chars)'; +} + +export function formatTimestamp(timestamp: string | Date): string { + try { + const date = new Date(timestamp); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + + if (diff < 60000) return 'Just now'; + if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; + if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; + + return date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + hour12: true + }); + } catch { + return String(timestamp); + } +} + +/** + * Format a duration in milliseconds to a human-readable short string (e.g. "3s", "5m", "2h") + */ +export function formatDuration(milliseconds: number): string { + if (milliseconds < 60000) return `${Math.round(milliseconds / 1000)}s`; + if (milliseconds < 3600000) return `${Math.round(milliseconds / 60000)}m`; + return `${Math.round(milliseconds / 3600000)}h`; +} + +/** + * Format a timestamp string to a short time (e.g. "02:30 PM") + */ +export function formatTime(timestamp: string): string { + try { + return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }); + } catch { + return timestamp; + } +} + +/** + * Format a timestamp to date string using toLocaleDateString() + */ +export function formatDate(timestamp: string | Date): string { + try { + return new Date(timestamp).toLocaleDateString(); + } catch { + return String(timestamp); + } +} + +/** + * Format a timestamp to time string using toLocaleTimeString() + */ +export function formatTimeOfDay(timestamp: string | Date): string { + try { + return new Date(timestamp).toLocaleTimeString(); + } catch { + return String(timestamp); + } +} + +export function formatFileSize(bytes: number): string { + const sizes = ['B', 'KB', 'MB', 'GB']; + if (bytes === 0) return '0 B'; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; +} + +export interface XmlSegment { + type: 'text' | 'xml'; + content: string; + tag?: string; + innerContent?: string; +} + +export function parseXmlBlocks(text: string): XmlSegment[] { + if (!text) return []; + + const result: XmlSegment[] = []; + const tagPattern = /<([a-z][a-z0-9_-]*(?:\s[^>]*)?)>/gi; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = tagPattern.exec(text)) !== null) { + const fullOpenTag = match[0]; + const tagContent = match[1]; + const tagName = tagContent.split(/\s/)[0]; + const openPos = match.index; + + const closeTag = ``; + let depth = 1; + let searchPos = openPos + fullOpenTag.length; + + while (depth > 0 && searchPos < text.length) { + const nextOpen = text.indexOf(`<${tagName}`, searchPos); + const nextClose = text.indexOf(closeTag, searchPos); + + if (nextClose === -1) break; + + if (nextOpen !== -1 && nextOpen < nextClose) { + const charAfterName = text[nextOpen + tagName.length + 1]; + if (charAfterName === '>' || charAfterName === ' ' || charAfterName === '\n') { + depth++; + } + searchPos = nextOpen + tagName.length + 2; + } else { + depth--; + if (depth === 0) { + const innerStart = openPos + fullOpenTag.length; + const innerEnd = nextClose; + const blockEnd = nextClose + closeTag.length; + + if (openPos > lastIndex) { + const preceding = text.substring(lastIndex, openPos).trim(); + if (preceding) result.push({ type: 'text', content: preceding }); + } + + result.push({ + type: 'xml', + content: text.substring(openPos, blockEnd), + tag: tagName, + innerContent: text.substring(innerStart, innerEnd) + }); + + lastIndex = blockEnd; + tagPattern.lastIndex = blockEnd; + } else { + searchPos = nextClose + closeTag.length; + } + } + } + + if (depth > 0) { + tagPattern.lastIndex = openPos + fullOpenTag.length; + } + } + + if (lastIndex < text.length) { + const remaining = text.substring(lastIndex).trim(); + if (remaining) result.push({ type: 'text', content: remaining }); + } + + return result; +} + +export function hasCustomXmlBlocks(text: string): boolean { + return /<[a-z][a-z0-9_-]*(?:\s[^>]*)?>[\s\S]*?<\/[a-z][a-z0-9_-]*>/i.test(text); +} + +export function getXmlTagStyle(tag: string): { bg: string; border: string; headerBg: string; text: string; icon: string } { + if (/^(system-reminder|thinking_mode|reasoning_effort|antml_thinking_mode|fast_mode_info|claude[A-Z_-])/.test(tag)) { + return { bg: 'bg-amber-50', border: 'border-amber-200', headerBg: 'bg-amber-100', text: 'text-amber-800', icon: 'settings' }; + } + if (/^(functions?|function_calls?|antml_function|antml_invoke|antml_parameter|tool)/.test(tag)) { + return { bg: 'bg-emerald-50', border: 'border-emerald-200', headerBg: 'bg-emerald-100', text: 'text-emerald-800', icon: 'wrench' }; + } + if (/^(local-command|command-|user-prompt)/.test(tag)) { + return { bg: 'bg-blue-50', border: 'border-blue-200', headerBg: 'bg-blue-100', text: 'text-blue-800', icon: 'terminal' }; + } + if (/^(types?|examples?|skills?|context|references?)/.test(tag)) { + return { bg: 'bg-purple-50', border: 'border-purple-200', headerBg: 'bg-purple-100', text: 'text-purple-800', icon: 'database' }; + } + return { bg: 'bg-gray-50', border: 'border-gray-200', headerBg: 'bg-gray-100', text: 'text-gray-700', icon: 'code' }; +} + +function isTextContentBlock(value: unknown): value is TextContentBlock { + return !!value && typeof value === 'object' && 'type' in value && value.type === 'text' && 'text' in value && typeof value.text === 'string'; +} + +export function createContentPreview(content: MessageContent | unknown, maxLength: number = 100): string { + if (typeof content === 'string') { + return content.length > maxLength ? content.substring(0, maxLength) + '...' : content; + } + + if (Array.isArray(content)) { + const textContent = content.find((item) => isTextContentBlock(item))?.text || ''; + if (textContent) { + return textContent.length > maxLength ? textContent.substring(0, maxLength) + '...' : textContent; + } + return `${content.length} content blocks`; + } + + if (content && typeof content === 'object') { + if ('text' in content && typeof content.text === 'string') { + return content.text.length > maxLength ? content.text.substring(0, maxLength) + '...' : content.text; + } + return 'Complex content'; + } + + return 'No content'; +} diff --git a/shared/frontend/types.ts b/shared/frontend/types.ts new file mode 100644 index 0000000..5b4bc72 --- /dev/null +++ b/shared/frontend/types.ts @@ -0,0 +1,307 @@ +export type JsonPrimitive = string | number | boolean | null; +export type JsonValue = JsonPrimitive | JsonObject | JsonValue[]; +export type JsonObject = { [key: string]: JsonValue }; + +export interface HeaderRule { + header: string; + action: 'block' | 'set' | 'replace'; + value?: string; + find?: string; + enabled: boolean; +} + +export interface ProxySettings { + requestHeaderRules: HeaderRule[]; + responseHeaderRules: HeaderRule[]; +} + +export interface CacheControl { + type: string; +} + +export interface SystemMessage { + text: string; + type: string; + cache_control?: CacheControl; +} + +export interface ToolInput extends Record { + file_path?: string; + old_string?: string; + new_string?: string; + command?: string; + description?: string; + offset?: number; + limit?: number; + replace_all?: boolean; + content?: string; + pattern?: string; + glob?: string; + path?: string; + prompt?: string; + todos?: TodoItem[]; +} + +export interface ToolParameterSchema extends Record { + type?: string | string[]; + properties?: Record>; + required?: string[]; +} + +export interface ToolDefinition { + name: string; + description: string; + input_schema?: ToolParameterSchema; + parameters?: ToolParameterSchema; +} + +export interface TodoItem extends Record { + task?: string; + description?: string; + content?: string; + title?: string; + text?: string; + priority: 'high' | 'medium' | 'low'; + status: 'pending' | 'in_progress' | 'completed'; +} + +interface BaseContentBlock extends Record { + type: string; + text?: string; + name?: string; + id?: string; + input?: ToolInput; + thinking?: string; + content?: unknown; + tool_use_id?: string; + tool_call_id?: string; + is_error?: boolean; +} + +export interface TextContentBlock extends BaseContentBlock { + type: 'text'; + text: string; +} + +export interface ToolUseContentBlock extends BaseContentBlock { + type: 'tool_use'; + id?: string; + name?: string; + input?: ToolInput; + text?: string; +} + +export interface ToolResultContentBlock extends BaseContentBlock { + type: 'tool_result'; + id?: string; + tool_use_id?: string; + tool_call_id?: string; + content?: unknown; + text?: string; + is_error?: boolean; +} + +export interface ImageContentBlock extends BaseContentBlock { + type: 'image'; + source?: { + type: string; + media_type: string; + data: string; + }; + data?: string; + media_type?: string; +} + +export interface ThinkingContentBlock extends BaseContentBlock { + type: 'thinking'; + thinking?: string; +} + +export interface GenericContentBlock extends BaseContentBlock { + type: string; +} + +export type ContentBlock = + | TextContentBlock + | ToolUseContentBlock + | ToolResultContentBlock + | ImageContentBlock + | ThinkingContentBlock + | GenericContentBlock; + +export type MessageContent = string | ContentBlock | ContentBlock[] | Record; + +export interface RequestMessage { + role: string; + content: MessageContent; +} + +export interface PromptGrade { + score: number; + maxScore?: number; + feedback: string; + improvedPrompt: string; + criteria: Record; + gradingTimestamp: string; + isProcessing?: boolean; +} + +export interface Request { + id: string; + conversationId?: string; + turnNumber?: number; + isRoot?: boolean; + timestamp: string; + method: string; + endpoint: string; + headers: Record; + originalModel?: string; + routedModel?: string; + body?: { + model?: string; + messages?: RequestMessage[]; + system?: SystemMessage[]; + tools?: ToolDefinition[]; + max_tokens?: number; + temperature?: number; + stream?: boolean; + }; + response?: { + statusCode: number; + headers: Record; + body?: { + usage?: { + input_tokens?: number; + output_tokens?: number; + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; + service_tier?: string; + }; + content?: MessageContent; + [key: string]: unknown; + }; + bodyText?: string; + responseTime: number; + streamingChunks?: string[]; + isStreaming: boolean; + completedAt: string; + }; + promptGrade?: PromptGrade; +} + +export interface ConversationSummary { + id: string; + requestCount: number; + startTime: string; + lastActivity: string; + duration: number; + firstMessage: string; + lastMessage: string; + projectPath: string; + projectName: string; +} + +export interface Conversation { + sessionId: string; + projectPath: string; + projectName: string; + messages: Array<{ + parentUuid: string | null; + isSidechain: boolean; + userType: string; + cwd: string; + sessionId: string; + version: string; + type: 'user' | 'assistant' | 'system'; + message: unknown; + uuid: string; + timestamp: string; + }>; + startTime: string; + endTime: string; + messageCount: number; +} + +export interface RequestSummary { + requestId: string; + timestamp: string; + method: string; + endpoint: string; + model?: string; + originalModel?: string; + routedModel?: string; + statusCode?: number; + responseTime?: number; + usage?: { + input_tokens?: number; + output_tokens?: number; + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; + service_tier?: string; + }; + conversationHash?: string; + messageCount?: number; + stopReason?: string; +} + +export interface ConversationGroup { + conversationHash: string; + latestRequest: RequestSummary; + turnCount: number; + totalTokens: number; + totalResponseTime: number; + firstTimestamp: string; + lastTimestamp: string; + requestIds: string[]; +} + +export interface DashboardStats { + dailyStats: DailyTokens[]; +} + +export interface DailyTokens { + date: string; + tokens: number; + requests: number; + models?: Record; +} + +export interface HourlyStatsResponse { + hourlyStats: HourlyTokens[]; + todayTokens: number; + todayRequests: number; + avgResponseTime: number; +} + +export interface HourlyTokens { + hour: number; + label?: string; + tokens: number; + requests: number; + models?: Record; +} + +export interface ModelStatsResponse { + modelStats: ModelTokens[]; +} + +export interface ModelTokens { + model: string; + tokens: number; + requests: number; +} + +export interface UsageStats { + total_requests: number; + total_input_tokens: number; + total_output_tokens: number; + total_cache_tokens: number; + requests_by_model: Record; + start_date?: string; + end_date?: string; +} diff --git a/shared/server/dashboard_auth.ts b/shared/server/dashboard_auth.ts new file mode 100644 index 0000000..e5af0fe --- /dev/null +++ b/shared/server/dashboard_auth.ts @@ -0,0 +1,49 @@ +const DASHBOARD_USER = 'admin'; +const REALM = 'Claude Code Proxy'; + +function decodeBasicAuthHeader(authHeader: string): { user: string; pass: string } | null { + if (!authHeader.startsWith('Basic ')) { + return null; + } + + try { + const decoded = atob(authHeader.slice(6)); + const colonIndex = decoded.indexOf(':'); + if (colonIndex === -1) { + return null; + } + return { + user: decoded.slice(0, colonIndex), + pass: decoded.slice(colonIndex + 1), + }; + } catch { + return null; + } +} + +export function isDashboardAuthorized(authHeader: string, password: string): boolean { + if (!password) { + return true; + } + + const credentials = decodeBasicAuthHeader(authHeader); + return credentials !== null && credentials.user === DASHBOARD_USER && credentials.pass === password; +} + +export function backendAuthHeaders(password: string): Record { + if (!password) { + return {}; + } + + const credentials = btoa(`${DASHBOARD_USER}:${password}`); + return { Authorization: `Basic ${credentials}` }; +} + +export function dashboardUnauthorizedResponse(): Response { + return new Response('Unauthorized', { + status: 401, + headers: { + 'WWW-Authenticate': `Basic realm="${REALM}"`, + }, + }); +} diff --git a/svelte/package-lock.json b/svelte/package-lock.json new file mode 100644 index 0000000..ab0b1df --- /dev/null +++ b/svelte/package-lock.json @@ -0,0 +1,2851 @@ +{ + "name": "claude-code-proxy-svelte", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "claude-code-proxy-svelte", + "version": "1.0.0", + "dependencies": { + "chart.js": "^4.5.1", + "lucide-svelte": "^0.522.0" + }, + "devDependencies": { + "@sveltejs/adapter-node": "^5.2.12", + "@sveltejs/kit": "^2.21.1", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@types/node": "^25.5.0", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "svelte": "^5.33.0", + "svelte-check": "^4.2.1", + "tailwindcss": "^3.4.4", + "typescript": "^5.1.6", + "vite": "^6.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz", + "integrity": "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.4.tgz", + "integrity": "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.59.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.55.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz", + "integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.6.4", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", + "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@typescript-eslint/types": "^8.2.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/lucide-svelte": { + "version": "0.522.0", + "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.522.0.tgz", + "integrity": "sha512-5/Xrf9kx60WaqLqyEFvJVh3HEi6t05PFTNczYsMkH4AQPKr2BW7HctlgDYfUwScz0/OHVlHG+gfgklymMtK2UQ==", + "license": "ISC", + "peerDependencies": { + "svelte": "^3 || ^4 || ^5.0.0-next.42" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "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/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "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.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz", + "integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.54.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.54.0.tgz", + "integrity": "sha512-TTDxwYnHkova6Wsyj1PGt9TByuWqvMoeY1bQiuAf2DM/JeDSMw7FjRKzk8K/5mJ99vGOKhbCqTDpyAKwjp4igg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.2", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz", + "integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", + "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/svelte/package.json b/svelte/package.json new file mode 100644 index 0000000..41a58a9 --- /dev/null +++ b/svelte/package.json @@ -0,0 +1,32 @@ +{ + "name": "claude-code-proxy-svelte", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "svelte-kit sync && vite dev", + "build": "svelte-kit sync && vite build", + "preview": "svelte-kit sync && vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/adapter-node": "^5.2.12", + "@sveltejs/kit": "^2.21.1", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@types/node": "^25.5.0", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "svelte": "^5.33.0", + "svelte-check": "^4.2.1", + "tailwindcss": "^3.4.4", + "typescript": "^5.1.6", + "vite": "^6.0.0" + }, + "dependencies": { + "chart.js": "^4.5.1", + "lucide-svelte": "^0.522.0" + }, + "type": "module", + "engines": { + "node": ">=20.0.0" + } +} diff --git a/svelte/postcss.config.js b/svelte/postcss.config.js new file mode 100644 index 0000000..0f77216 --- /dev/null +++ b/svelte/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/svelte/src/app.css b/svelte/src/app.css new file mode 100644 index 0000000..d47763b --- /dev/null +++ b/svelte/src/app.css @@ -0,0 +1,333 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +* { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +body { + background-color: #fafafa; + color: #111; +} + +html.dark body { + background-color: #0f172a; + color: #e2e8f0; +} + +@layer utilities { + .line-clamp-2 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + } +} + +.code-block { + font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; + font-size: 0.75rem; + line-height: 1.5; + background: #f5f5f5; + border: 1px solid #e5e5e5; + border-radius: 4px; +} + +html.dark .code-block { + background: #1e293b; + border-color: #334155; +} + +.scrollbar-custom { + scrollbar-width: thin; + scrollbar-color: #ddd #f5f5f5; +} + +html.dark .scrollbar-custom { + scrollbar-color: #475569 #1e293b; +} + +.scrollbar-custom::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.scrollbar-custom::-webkit-scrollbar-track { + background: #f5f5f5; + border-radius: 3px; +} + +html.dark .scrollbar-custom::-webkit-scrollbar-track { + background: #1e293b; +} + +.scrollbar-custom::-webkit-scrollbar-thumb { + background: #ddd; + border-radius: 3px; +} + +html.dark .scrollbar-custom::-webkit-scrollbar-thumb { + background: #475569; +} + +.scrollbar-custom::-webkit-scrollbar-thumb:hover { + background: #ccc; +} + +html.dark .scrollbar-custom::-webkit-scrollbar-thumb:hover { + background: #64748b; +} + +/* --- Nav active/inactive states (immune to dark mode CSS overrides) --- */ +.nav-active { + background-color: #111827; /* gray-900 */ + color: #fff; +} + +html.dark .nav-active { + background-color: #e5e7eb; /* gray-200 */ + color: #111827; +} + +.nav-inactive { + color: #4b5563; /* gray-600 */ +} + +.nav-inactive:hover { + background-color: #f3f4f6; /* gray-100 */ +} + +html.dark .nav-inactive { + color: #9ca3af; /* gray-400 */ +} + +html.dark .nav-inactive:hover { + background-color: #1f2937; /* gray-800 */ +} + +/* ============================================================================ + Dark Mode Overrides + + These override Tailwind utility classes when html.dark is present. + This approach avoids adding dark: variants to every component. + ============================================================================ */ + +/* --- Background colors --- */ +html.dark .bg-white { background-color: #1e293b; } +html.dark .bg-gray-50 { background-color: #0f172a; } +html.dark .bg-gray-100 { background-color: #1e293b; } +html.dark .bg-gray-200 { background-color: #334155; } + +/* Accent backgrounds - subtle dark variants */ +html.dark .bg-blue-50 { background-color: rgba(59, 130, 246, 0.1); } +html.dark .bg-blue-100 { background-color: rgba(59, 130, 246, 0.2); } +html.dark .bg-indigo-50 { background-color: rgba(99, 102, 241, 0.1); } +html.dark .bg-indigo-100 { background-color: rgba(99, 102, 241, 0.15); } +html.dark .bg-purple-50 { background-color: rgba(147, 51, 234, 0.1); } +html.dark .bg-purple-100 { background-color: rgba(147, 51, 234, 0.15); } +html.dark .bg-green-50 { background-color: rgba(34, 197, 94, 0.1); } +html.dark .bg-green-100 { background-color: rgba(34, 197, 94, 0.15); } +html.dark .bg-emerald-50 { background-color: rgba(16, 185, 129, 0.1); } +html.dark .bg-emerald-100 { background-color: rgba(16, 185, 129, 0.15); } +html.dark .bg-red-50 { background-color: rgba(239, 68, 68, 0.1); } +html.dark .bg-red-100 { background-color: rgba(239, 68, 68, 0.15); } +html.dark .bg-yellow-50 { background-color: rgba(234, 179, 8, 0.1); } +html.dark .bg-yellow-100 { background-color: rgba(234, 179, 8, 0.15); } +html.dark .bg-amber-50 { background-color: rgba(245, 158, 11, 0.1); } +html.dark .bg-amber-100 { background-color: rgba(245, 158, 11, 0.15); } +html.dark .bg-orange-100 { background-color: rgba(249, 115, 22, 0.15); } +html.dark .bg-teal-50 { background-color: rgba(20, 184, 166, 0.1); } +html.dark .bg-slate-50 { background-color: rgba(100, 116, 139, 0.1); } + +/* Gradient backgrounds */ +html.dark .bg-gradient-to-r.from-blue-50 { --tw-gradient-from: rgba(59, 130, 246, 0.1); } +html.dark .bg-gradient-to-r.to-indigo-50 { --tw-gradient-to: rgba(99, 102, 241, 0.1); } +html.dark .bg-gradient-to-r.from-indigo-50 { --tw-gradient-from: rgba(99, 102, 241, 0.1); } +html.dark .bg-gradient-to-r.to-purple-50 { --tw-gradient-to: rgba(147, 51, 234, 0.1); } +html.dark .bg-gradient-to-r.from-purple-50 { --tw-gradient-from: rgba(147, 51, 234, 0.1); } +html.dark .bg-gradient-to-r.to-blue-50 { --tw-gradient-to: rgba(59, 130, 246, 0.1); } +html.dark .bg-gradient-to-r.from-emerald-50 { --tw-gradient-from: rgba(16, 185, 129, 0.1); } +html.dark .bg-gradient-to-r.to-green-50 { --tw-gradient-to: rgba(34, 197, 94, 0.1); } +html.dark .bg-gradient-to-r.from-red-50 { --tw-gradient-from: rgba(239, 68, 68, 0.1); } +html.dark .bg-gradient-to-r.to-pink-50 { --tw-gradient-to: rgba(236, 72, 153, 0.1); } + +/* --- Text colors --- */ +html.dark .text-gray-900 { color: #f1f5f9; } +html.dark .text-gray-800 { color: #e2e8f0; } +html.dark .text-gray-700 { color: #cbd5e1; } +html.dark .text-gray-600 { color: #94a3b8; } +html.dark .text-gray-500 { color: #64748b; } +html.dark .text-gray-400 { color: #64748b; } + +/* Accent text colors - brighten for dark backgrounds */ +html.dark .text-blue-700 { color: #93c5fd; } +html.dark .text-blue-600 { color: #60a5fa; } +html.dark .text-indigo-900 { color: #c7d2fe; } +html.dark .text-indigo-700 { color: #a5b4fc; } +html.dark .text-indigo-600 { color: #818cf8; } +html.dark .text-purple-900 { color: #e9d5ff; } +html.dark .text-purple-700 { color: #c084fc; } +html.dark .text-purple-600 { color: #a855f7; } +html.dark .text-green-900 { color: #bbf7d0; } +html.dark .text-green-800 { color: #86efac; } +html.dark .text-green-700 { color: #4ade80; } +html.dark .text-green-600 { color: #22c55e; } +html.dark .text-emerald-900 { color: #a7f3d0; } +html.dark .text-emerald-700 { color: #34d399; } +html.dark .text-emerald-600 { color: #10b981; } +html.dark .text-red-900 { color: #fecaca; } +html.dark .text-red-800 { color: #fca5a5; } +html.dark .text-red-700 { color: #f87171; } +html.dark .text-red-600 { color: #ef4444; } +html.dark .text-yellow-900 { color: #fef08a; } +html.dark .text-yellow-700 { color: #facc15; } +html.dark .text-yellow-600 { color: #eab308; } +html.dark .text-amber-900 { color: #fde68a; } +html.dark .text-amber-800 { color: #fbbf24; } +html.dark .text-amber-700 { color: #f59e0b; } +html.dark .text-amber-600 { color: #d97706; } +html.dark .text-teal-600 { color: #2dd4bf; } +html.dark .text-blue-900 { color: #bfdbfe; } +html.dark .text-slate-800 { color: #e2e8f0; } +html.dark .text-slate-700 { color: #cbd5e1; } +html.dark .text-slate-600 { color: #94a3b8; } + +/* --- Border colors --- */ +html.dark .border-gray-200 { border-color: #334155; } +html.dark .border-gray-100 { border-color: #1e293b; } +html.dark .border-gray-300 { border-color: #475569; } +html.dark .border-blue-200 { border-color: rgba(59, 130, 246, 0.3); } +html.dark .border-indigo-200 { border-color: rgba(99, 102, 241, 0.3); } +html.dark .border-purple-200 { border-color: rgba(147, 51, 234, 0.3); } +html.dark .border-green-200 { border-color: rgba(34, 197, 94, 0.3); } +html.dark .border-green-100 { border-color: rgba(34, 197, 94, 0.15); } +html.dark .border-emerald-200 { border-color: rgba(16, 185, 129, 0.3); } +html.dark .border-emerald-100 { border-color: rgba(16, 185, 129, 0.15); } +html.dark .border-red-200 { border-color: rgba(239, 68, 68, 0.3); } +html.dark .border-yellow-200 { border-color: rgba(234, 179, 8, 0.3); } +html.dark .border-amber-200 { border-color: rgba(245, 158, 11, 0.3); } +html.dark .border-orange-200 { border-color: rgba(249, 115, 22, 0.3); } +html.dark .border-slate-200 { border-color: #334155; } +html.dark .border-slate-100 { border-color: #1e293b; } +html.dark .border-blue-100 { border-color: rgba(59, 130, 246, 0.15); } +html.dark .border-gray-700 { border-color: #475569; } + +/* Left-accent borders */ +html.dark .border-l-blue-500 { border-left-color: #3b82f6; } +html.dark .border-l-gray-500 { border-left-color: #64748b; } +html.dark .border-l-amber-500 { border-left-color: #f59e0b; } +html.dark .border-l-emerald-500 { border-left-color: #10b981; } +html.dark .border-l-red-500 { border-left-color: #ef4444; } +html.dark .border-l-blue-500 { border-left-color: #3b82f6; } + +/* --- Divide colors --- */ +html.dark .divide-gray-200 > :not([hidden]) ~ :not([hidden]) { border-color: #334155; } +html.dark .divide-gray-100 > :not([hidden]) ~ :not([hidden]) { border-color: #1e293b; } +html.dark .divide-slate-100 > :not([hidden]) ~ :not([hidden]) { border-color: #1e293b; } + +/* --- Hover overrides --- */ +html.dark .hover\:bg-gray-50:hover { background-color: #1e293b; } +html.dark .hover\:bg-gray-100:hover { background-color: #334155; } +html.dark .hover\:bg-gray-200:hover { background-color: #475569; } +html.dark .hover\:bg-blue-50:hover { background-color: rgba(59, 130, 246, 0.15); } +html.dark .hover\:bg-red-50:hover { background-color: rgba(239, 68, 68, 0.15); } +html.dark .hover\:bg-purple-50:hover { background-color: rgba(147, 51, 234, 0.15); } +html.dark .hover\:bg-white\/50:hover { background-color: rgba(30, 41, 59, 0.5); } +html.dark .hover\:bg-slate-100\/50:hover { background-color: rgba(30, 41, 59, 0.5); } +html.dark .hover\:bg-amber-100\/50:hover { background-color: rgba(245, 158, 11, 0.15); } +html.dark .hover\:bg-emerald-100\/50:hover { background-color: rgba(16, 185, 129, 0.15); } + +html.dark .hover\:text-gray-600:hover { color: #94a3b8; } +html.dark .hover\:text-gray-700:hover { color: #cbd5e1; } +html.dark .hover\:text-gray-800:hover { color: #e2e8f0; } +html.dark .hover\:text-gray-900:hover { color: #f1f5f9; } +html.dark .hover\:text-blue-800:hover { color: #93c5fd; } +html.dark .hover\:text-indigo-800:hover { color: #a5b4fc; } +html.dark .hover\:text-amber-800:hover { color: #fcd34d; } +html.dark .hover\:text-emerald-800:hover { color: #6ee7b7; } +html.dark .hover\:text-red-800:hover { color: #fca5a5; } + +html.dark .hover\:shadow-md:hover { --tw-shadow-color: rgba(0, 0, 0, 0.3); } + +/* --- Ring overrides --- */ +html.dark .ring-blue-200\/30 { --tw-ring-color: rgba(59, 130, 246, 0.15); } + +/* --- Focus overrides --- */ +html.dark .focus\:ring-blue-500:focus { --tw-ring-color: #3b82f6; } +html.dark .focus\:ring-emerald-500:focus { --tw-ring-color: #10b981; } + +/* --- Shadow adjustments --- */ +html.dark .shadow-sm { --tw-shadow-color: rgba(0, 0, 0, 0.2); } +html.dark .shadow-md { --tw-shadow-color: rgba(0, 0, 0, 0.3); } +html.dark .shadow-2xl { --tw-shadow-color: rgba(0, 0, 0, 0.5); } + +/* --- Modal backdrops --- */ +html.dark .bg-gray-900\/70 { background-color: rgba(0, 0, 0, 0.8); } + +/* --- Chat page specific --- */ +html.dark .bg-blue-500 { background-color: #2563eb; } +html.dark .bg-gray-200.text-gray-900 { background-color: #334155; color: #e2e8f0; } + +/* Chat bubble colors */ +html.dark .bg-blue-100 { background-color: rgba(59, 130, 246, 0.2); } + +/* --- Prose / formatted content --- */ +html.dark .prose { color: #cbd5e1; } +html.dark .prose pre { background-color: #0f172a; border-color: #334155; } + +/* --- Form elements --- */ +html.dark input, +html.dark select, +html.dark textarea { + background-color: #1e293b; + border-color: #475569; + color: #e2e8f0; +} + +html.dark input::placeholder, +html.dark textarea::placeholder { + color: #64748b; +} + +html.dark option { + background-color: #1e293b; + color: #e2e8f0; +} + +/* --- Code blocks (already dark, keep them) --- */ +html.dark .bg-gray-900 { background-color: #0f172a; } +html.dark .bg-gray-800 { background-color: #1e293b; } + +/* --- Misc --- */ +html.dark .bg-black { background-color: #000; } +html.dark .brightness-95 { filter: brightness(1.05); } + +/* Hover on table rows */ +html.dark .hover\:bg-gray-800\/50:hover { background-color: rgba(30, 41, 59, 0.5); } +html.dark .hover\:bg-gray-100:hover { background-color: #334155; } +html.dark .hover\:bg-gray-50:hover { background-color: #1e293b; } + +/* Sticky headers */ +html.dark .bg-gray-50.sticky { background-color: #0f172a; } + +/* Active/selected states for model filter buttons */ +html.dark .bg-transparent { background-color: transparent; } diff --git a/svelte/src/app.html b/svelte/src/app.html new file mode 100644 index 0000000..f3cd474 --- /dev/null +++ b/svelte/src/app.html @@ -0,0 +1,20 @@ + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/svelte/src/hooks.server.ts b/svelte/src/hooks.server.ts new file mode 100644 index 0000000..7282781 --- /dev/null +++ b/svelte/src/hooks.server.ts @@ -0,0 +1,16 @@ +import type { Handle } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; +import { dashboardUnauthorizedResponse, isDashboardAuthorized } from '../../shared/server/dashboard_auth'; + +export const handle: Handle = async ({ event, resolve }) => { + const password = env.DASHBOARD_PASSWORD || ''; + + if (password) { + const authHeader = event.request.headers.get('Authorization') || ''; + if (!isDashboardAuthorized(authHeader, password)) { + return dashboardUnauthorizedResponse(); + } + } + + return resolve(event); +}; diff --git a/svelte/src/lib/api.ts b/svelte/src/lib/api.ts new file mode 100644 index 0000000..3094e41 --- /dev/null +++ b/svelte/src/lib/api.ts @@ -0,0 +1,225 @@ +import type { + Request, ConversationSummary, Conversation, + RequestSummary, DashboardStats, HourlyStatsResponse, + ModelStatsResponse, UsageStats, ProxySettings, + PromptGrade, RequestMessage, SystemMessage +} from './types'; + +const API_BASE = '/api'; + +type RequestListItem = Omit & { id?: string; requestId?: string }; +type RequestsResponse = { requests?: RequestListItem[]; total?: number }; +type ConversationsResponse = { conversations: ConversationSummary[]; hasMore?: boolean; total?: number }; +type RequestSummaryResponse = { requests: RequestSummary[]; total: number }; +type RequestDetailResponse = { request: Request; fullId: string }; +type LatestRequestDateResponse = { latestDate: string | null }; +type OrganizationsResponse = { organizations?: string[] }; + +export async function fetchRequests( + page: number = 1, + limit: number = 50, + model: string = 'all' +): Promise<{ requests: Request[]; hasMore: boolean; total: number }> { + const url = new URL(`${API_BASE}/requests`, window.location.origin); + url.searchParams.append('page', page.toString()); + url.searchParams.append('limit', limit.toString()); + if (model !== 'all') { + url.searchParams.append('model', model); + } + + const response = await fetch(url.toString()); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + + const data: RequestsResponse = await response.json(); + const fetchedRequests = data.requests || []; + const mappedRequests: Request[] = fetchedRequests.map((req) => ({ + ...req, + id: req.id || req.requestId || req.timestamp + })); + + return { requests: mappedRequests, hasMore: mappedRequests.length === limit, total: data.total || 0 }; +} + +export async function fetchConversations( + page: number = 1, + limit: number = 50, + model: string = 'all' +): Promise<{ conversations: ConversationSummary[]; hasMore: boolean; total?: number }> { + const url = new URL(`${API_BASE}/conversations`, window.location.origin); + url.searchParams.append('page', page.toString()); + url.searchParams.append('limit', limit.toString()); + if (model !== 'all') { + url.searchParams.append('model', model); + } + + const response = await fetch(url.toString()); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + + const data: ConversationsResponse = await response.json(); + return { + conversations: data.conversations, + hasMore: typeof data.hasMore === 'boolean' ? data.hasMore : data.conversations.length === limit, + total: typeof data.total === 'number' ? data.total : undefined + }; +} + +export async function fetchConversationDetail( + conversationId: string, + projectPath: string +): Promise { + const response = await fetch( + `${API_BASE}/conversations/${encodeURIComponent(conversationId)}?project=${encodeURIComponent(projectPath)}` + ); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json(); +} + +export async function deleteRequests(): Promise { + const response = await fetch(`${API_BASE}/requests`, { method: 'DELETE' }); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); +} + +export async function gradePrompt( + messages: RequestMessage[], + systemMessages: SystemMessage[], + requestId: string +): Promise { + const response = await fetch(`${API_BASE}/grade-prompt`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages, systemMessages, requestId }) + }); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json(); +} + +// New summary endpoint — lightweight request list for fast rendering +export async function fetchRequestsSummary( + model: string = 'all', + startTime?: string, + endTime?: string, + offset: number = 0, + limit: number = 0 +): Promise<{ requests: RequestSummary[]; total: number }> { + const url = new URL(`${API_BASE}/requests/summary`, window.location.origin); + if (model !== 'all') url.searchParams.append('model', model); + if (startTime) url.searchParams.append('start', startTime); + if (endTime) url.searchParams.append('end', endTime); + if (offset > 0) url.searchParams.append('offset', offset.toString()); + if (limit > 0) url.searchParams.append('limit', limit.toString()); + + const response = await fetch(url.toString()); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json() as Promise; +} + +// Fetch a single request by ID (full detail) +export async function fetchRequestById( + requestId: string +): Promise<{ request: Request; fullId: string }> { + const response = await fetch(`${API_BASE}/requests/${encodeURIComponent(requestId)}`); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json() as Promise; +} + +// Get the latest request date +export async function fetchLatestRequestDate(): Promise<{ latestDate: string | null }> { + const response = await fetch(`${API_BASE}/requests/latest-date`); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json() as Promise; +} + +// Usage stats with date range and model filter +export async function fetchUsageStats( + startDate?: string, + endDate?: string, + model?: string, + org?: string +): Promise { + const url = new URL(`${API_BASE}/stats`, window.location.origin); + if (startDate) url.searchParams.append('start_date', startDate); + if (endDate) url.searchParams.append('end_date', endDate); + if (model && model !== 'all') url.searchParams.append('model', model); + if (org) url.searchParams.append('org', org); + + const response = await fetch(url.toString()); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json(); +} + +// Dashboard stats — daily token usage +export async function fetchDashboardStats( + startTime?: string, + endTime?: string, + org?: string +): Promise { + const url = new URL(`${API_BASE}/stats/dashboard`, window.location.origin); + if (startTime) url.searchParams.append('start', startTime); + if (endTime) url.searchParams.append('end', endTime); + if (org) url.searchParams.append('org', org); + + const response = await fetch(url.toString()); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json(); +} + +// Hourly stats for a date range +export async function fetchHourlyStats( + startTime: string, + endTime: string, + bucketMinutes: number = 60, + org?: string +): Promise { + const url = new URL(`${API_BASE}/stats/hourly`, window.location.origin); + url.searchParams.append('start', startTime); + url.searchParams.append('end', endTime); + if (bucketMinutes !== 60) { + url.searchParams.append('bucket', bucketMinutes.toString()); + } + if (org) url.searchParams.append('org', org); + + const response = await fetch(url.toString()); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json(); +} + +// Settings +export async function fetchSettings(): Promise { + const response = await fetch(`${API_BASE}/settings`); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json(); +} + +export async function saveSettings(settings: ProxySettings): Promise { + const response = await fetch(`${API_BASE}/settings`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(settings) + }); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json(); +} + +// Model breakdown for a date range +export async function fetchModelStats( + startTime: string, + endTime: string, + org?: string +): Promise { + const url = new URL(`${API_BASE}/stats/models`, window.location.origin); + url.searchParams.append('start', startTime); + url.searchParams.append('end', endTime); + if (org) url.searchParams.append('org', org); + + const response = await fetch(url.toString()); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json(); +} + +// Get distinct organization IDs +export async function fetchOrganizations(): Promise { + const response = await fetch(`${API_BASE}/stats/organizations`); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + const data: OrganizationsResponse = await response.json(); + return data.organizations || []; +} diff --git a/svelte/src/lib/auth.server.ts b/svelte/src/lib/auth.server.ts new file mode 100644 index 0000000..7f311da --- /dev/null +++ b/svelte/src/lib/auth.server.ts @@ -0,0 +1,32 @@ +import { env } from '$env/dynamic/private'; +import { error } from '@sveltejs/kit'; +import type { RequestEvent } from '@sveltejs/kit'; +import { + backendAuthHeaders as buildSharedBackendAuthHeaders, + isDashboardAuthorized +} from '../../../shared/server/dashboard_auth'; + +function getDashboardPassword(): string { + return env.DASHBOARD_PASSWORD || ''; +} + +/** + * Check basic auth on the incoming request. + * Throws a 401 error if auth is required and invalid. + */ +export function requireDashboardAuth(event: RequestEvent): void { + const password = getDashboardPassword(); + const authHeader = event.request.headers.get('Authorization') || ''; + if (isDashboardAuthorized(authHeader, password)) { + return; + } + + throw error(401, 'Unauthorized'); +} + +/** + * Returns headers to forward basic auth to the Go backend. + */ +export function backendAuthHeaders(): Record { + return buildSharedBackendAuthHeaders(getDashboardPassword()); +} diff --git a/svelte/src/lib/backend.server.ts b/svelte/src/lib/backend.server.ts new file mode 100644 index 0000000..a94fb10 --- /dev/null +++ b/svelte/src/lib/backend.server.ts @@ -0,0 +1,10 @@ +import { env } from '$env/dynamic/private'; +import { buildBackendURL as buildSharedBackendURL, resolveBackendOrigin } from '../../../shared/frontend/backend'; + +export function getBackendOrigin(): string { + return resolveBackendOrigin(env); +} + +export function buildBackendURL(path: string, searchParams?: URLSearchParams): string { + return buildSharedBackendURL(getBackendOrigin(), path, searchParams); +} diff --git a/svelte/src/lib/chat-formatters.ts b/svelte/src/lib/chat-formatters.ts new file mode 100644 index 0000000..1e53e0d --- /dev/null +++ b/svelte/src/lib/chat-formatters.ts @@ -0,0 +1,101 @@ +/** + * Date/time formatting utilities for the chat view. + * + * These provide iMessage-style and relative timestamp formatting used by the + * chat page and its sub-components. The shared `$lib/formatters` module has + * generic helpers (`formatTimestamp`, `formatTime`, etc.) but nothing that + * matches the specific "Today 3:42 PM" / "Yesterday 10:15 AM" / "Mon 2:30 PM" + * style needed here, so we keep these separate. + */ + +import type { RequestMessage } from '$lib/types'; + +// --------------------------------------------------------------------------- +// Core helpers +// --------------------------------------------------------------------------- + +/** Calendar-day difference (accounts for date boundaries, not raw 24h). */ +export function calendarDayDiff(date: Date, now: Date): number { + const d = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + const n = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + return Math.round((n.getTime() - d.getTime()) / 86400000); +} + +// --------------------------------------------------------------------------- +// iMessage-style timestamp +// --------------------------------------------------------------------------- + +export function formatImessageTimestamp(ts: string): string { + const date = new Date(ts); + const now = new Date(); + const diffDays = calendarDayDiff(date, now); + + const timeStr = date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); + + if (diffDays === 0) return `Today ${timeStr}`; + if (diffDays === 1) return `Yesterday ${timeStr}`; + if (diffDays < 7) { + const day = date.toLocaleDateString(undefined, { weekday: 'long' }); + return `${day} ${timeStr}`; + } + return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: diffDays > 365 ? 'numeric' : undefined }) + ' ' + timeStr; +} + +// --------------------------------------------------------------------------- +// Relative timestamp ("2 min ago", "Yesterday 3:42 PM") +// --------------------------------------------------------------------------- + +export function formatRelativeTimestamp(ts: string): string { + const date = new Date(ts); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffSec = Math.floor(diffMs / 1000); + const diffMin = Math.floor(diffSec / 60); + const diffHr = Math.floor(diffMin / 60); + const diffDays = calendarDayDiff(date, now); + const timeStr = date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); + + if (diffSec < 60) return 'just now'; + if (diffMin < 60) return `${diffMin} min ago`; + if (diffDays === 0 && diffHr < 2) return `${diffHr} hr ago`; + if (diffDays === 0) return `Today ${timeStr}`; + if (diffDays === 1) return `Yesterday ${timeStr}`; + if (diffDays < 7) { + return date.toLocaleDateString(undefined, { weekday: 'short' }) + ' ' + timeStr; + } + return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' + timeStr; +} + +// --------------------------------------------------------------------------- +// Turn/timestamp helpers for message arrays +// --------------------------------------------------------------------------- + +/** Whether a timestamp separator should be shown before message at index idx. */ +export function shouldShowTimestamp(messages: RequestMessage[], idx: number): boolean { + if (idx === 0) return true; + const prev = messages[idx - 1]; + const curr = messages[idx]; + if (prev.role === 'assistant' && curr.role === 'user') return true; + return false; +} + +/** Estimate a 1-based turn number for a message based on user->assistant pairs. */ +export function getTurnNumber(messages: RequestMessage[], idx: number): number { + let turn = 0; + for (let i = 0; i <= idx; i++) { + if (messages[i].role === 'user' && (i === 0 || messages[i - 1]?.role === 'assistant')) { + turn++; + } + } + return turn; +} + +/** Total turns in the message array. */ +export function getTotalTurns(messages: RequestMessage[]): number { + return getTurnNumber(messages, messages.length - 1); +} + +/** Format a short time string (HH:MM:SS). */ +export function formatTimeFull(ts: string): string { + return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); +} diff --git a/svelte/src/lib/chat-utils.ts b/svelte/src/lib/chat-utils.ts new file mode 100644 index 0000000..f20d8e7 --- /dev/null +++ b/svelte/src/lib/chat-utils.ts @@ -0,0 +1,242 @@ +/** + * Shared utilities for the chat view components. + * + * Contains type-guards, content-splitting logic, and label/icon/color helpers + * that are used across ChatMessage, ChatToolBlock, and the main chat page. + */ + +import { parseXmlBlocks, getXmlTagStyle } from '$lib/formatters'; +import type { + MessageContent as RenderableMessageContent, + RequestMessage, + ToolUseContentBlock, + ToolResultContentBlock, + TextContentBlock, + ContentBlock, + GenericContentBlock +} from '$lib/types'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface XmlOutsideBlock { + type: 'xml-block'; + tag: string; + content: string; + raw: string; +} + +export type OutsideItem = Exclude | XmlOutsideBlock | GenericContentBlock; + +// --------------------------------------------------------------------------- +// Type guards +// --------------------------------------------------------------------------- + +export function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +export function isTextBlock(value: unknown): value is TextContentBlock { + return isRecord(value) && value.type === 'text' && typeof value.text === 'string'; +} + +export function isToolUseBlock(value: unknown): value is ToolUseContentBlock { + return isRecord(value) && value.type === 'tool_use'; +} + +export function isToolResultBlock(value: unknown): value is ToolResultContentBlock { + return isRecord(value) && value.type === 'tool_result'; +} + +export function isXmlOutsideBlock(value: unknown): value is XmlOutsideBlock { + return isRecord(value) && value.type === 'xml-block' && typeof value.tag === 'string' && typeof value.content === 'string'; +} + +// --------------------------------------------------------------------------- +// Content splitting +// --------------------------------------------------------------------------- + +/** + * Split content: plain text goes in chat bubble, everything else outside. + * Parses XML-like blocks out of text so they render as outside items. + */ +export function splitContent(content: RenderableMessageContent | undefined): { chat: string | null; outside: OutsideItem[] } { + if (typeof content === 'string') { + return splitTextContent(content); + } + + if (!Array.isArray(content)) { + if (isTextBlock(content)) { + return splitTextContent(content.text || ''); + } + return content ? { chat: null, outside: [content as OutsideItem] } : { chat: null, outside: [] }; + } + + let chatParts: string[] = []; + const outside: OutsideItem[] = []; + + for (const item of content) { + if (isTextBlock(item)) { + const result = splitTextContent(item.text); + outside.push(...result.outside); + if (result.chat) chatParts.push(result.chat); + } else { + outside.push(item as OutsideItem); + } + } + + const chatText = chatParts.join('\n\n').trim(); + return { chat: chatText || null, outside }; +} + +function splitTextContent(text: string): { chat: string | null; outside: OutsideItem[] } { + const outside: OutsideItem[] = []; + const segments = parseXmlBlocks(text); + + if (segments.length === 0 || (segments.length === 1 && segments[0].type === 'text')) { + return { chat: text.trim() || null, outside: [] }; + } + + const textParts: string[] = []; + for (const seg of segments) { + if (seg.type === 'text') { + const trimmed = seg.content.trim(); + if (trimmed) textParts.push(trimmed); + } else if (seg.type === 'xml' && seg.tag) { + outside.push({ + type: 'xml-block', + tag: seg.tag, + content: seg.innerContent || '', + raw: seg.content + }); + } + } + + return { chat: textParts.join('\n\n').trim() || null, outside }; +} + +// --------------------------------------------------------------------------- +// Labels / icons / colors for outside blocks +// --------------------------------------------------------------------------- + +export function outsideLabel(item: OutsideItem): string { + if (isXmlOutsideBlock(item)) return item.tag || 'block'; + if (isToolUseBlock(item)) return typeof item.name === 'string' ? item.name : 'tool call'; + if (isToolResultBlock(item)) return item.is_error ? 'error' : 'result'; + if (item.type === 'thinking') return `thought (${(typeof item.thinking === 'string' ? item.thinking.length : 0).toLocaleString()} chars)`; + return typeof item.type === 'string' ? item.type : 'block'; +} + +export function outsideIconName(item: OutsideItem): string { + if (isXmlOutsideBlock(item) && item.tag) { + const style = getXmlTagStyle(item.tag); + return style.icon; + } + switch (item.type) { + case 'thinking': return 'brain'; + case 'tool_use': return 'terminal'; + case 'tool_result': return item.is_error ? 'alert-circle' : 'check-circle'; + default: return 'code'; + } +} + +export function outsideColor(item: OutsideItem): string { + if (isXmlOutsideBlock(item) && item.tag) { + const style = getXmlTagStyle(item.tag); + return style.text.replace('text-', 'text-').replace('-800', '-400') + ' hover:' + style.text; + } + switch (item.type) { + case 'thinking': return 'text-amber-400 hover:text-amber-600'; + case 'tool_use': return 'text-indigo-400 hover:text-indigo-600'; + case 'tool_result': return item.is_error ? 'text-red-400 hover:text-red-600' : 'text-emerald-400 hover:text-emerald-600'; + default: return 'text-gray-400 hover:text-gray-600'; + } +} + +// --------------------------------------------------------------------------- +// Model helpers +// --------------------------------------------------------------------------- + +export function getModelLabel(model?: string): { label: string; color: string } { + if (!model) return { label: 'API', color: 'text-gray-600' }; + if (model.includes('opus')) return { label: 'Opus', color: 'text-purple-600' }; + if (model.includes('sonnet')) return { label: 'Sonnet', color: 'text-indigo-600' }; + if (model.includes('haiku')) return { label: 'Haiku', color: 'text-teal-600' }; + if (model.includes('gpt')) return { label: 'GPT', color: 'text-green-600' }; + return { label: model.split('-')[0], color: 'text-gray-700' }; +} + +export function getStatusBadge(code?: number): string { + if (!code) return 'bg-gray-100 text-gray-500'; + if (code >= 200 && code < 300) return 'bg-green-100 text-green-700'; + if (code >= 400) return 'bg-red-100 text-red-700'; + return 'bg-yellow-100 text-yellow-700'; +} + +// --------------------------------------------------------------------------- +// Tool helpers +// --------------------------------------------------------------------------- + +export function buildToolResultMap(messages: RequestMessage[]): Map { + const map = new Map(); + if (!messages) return map; + for (const msg of messages) { + const content = msg.content; + if (!Array.isArray(content)) continue; + for (const item of content) { + if (isToolResultBlock(item) && item.tool_use_id) { + map.set(item.tool_use_id, item); + } + } + } + return map; +} + +export function toolInputSummary(item: ToolUseContentBlock): string { + if (!item?.input) return ''; + const input = item.input; + switch (item.name) { + case 'Read': return input.file_path || ''; + case 'Edit': return input.file_path || ''; + case 'Write': return input.file_path || ''; + case 'Bash': return (input.command || '').slice(0, 80); + case 'Grep': return input.pattern ? `"${input.pattern}"` : ''; + case 'Glob': return input.pattern || ''; + case 'Agent': return (input.prompt || '').slice(0, 80); + default: { + const first = Object.values(input).find((v) => typeof v === 'string'); + return typeof first === 'string' ? first.slice(0, 80) : ''; + } + } +} + +export function getToolResultContent(result: ToolResultContentBlock | undefined): string { + if (!result) return ''; + let content = result.content ?? result.text ?? ''; + if (Array.isArray(content)) { + return content + .map((c) => { + if (typeof c === 'string') return c; + if (isRecord(c) && typeof c.text === 'string') return c.text; + if (isRecord(c) && typeof c.content === 'string') return c.content; + return ''; + }) + .filter(Boolean) + .join('\n'); + } + if (isRecord(content)) { + if (typeof content.text === 'string') return content.text; + if (typeof content.content === 'string') return content.content; + return JSON.stringify(content, null, 2); + } + return String(content); +} + +export function toolResultBrief(result: ToolResultContentBlock | undefined): { text: string; isError: boolean; chars: number } { + const isError = result?.is_error || false; + const content = getToolResultContent(result); + const chars = content.length; + if (isError) return { text: `error (${chars.toLocaleString()} chars)`, isError, chars }; + return { text: `${chars.toLocaleString()} chars`, isError, chars }; +} diff --git a/svelte/src/lib/components/ChartCanvas.svelte b/svelte/src/lib/components/ChartCanvas.svelte new file mode 100644 index 0000000..8ec9736 --- /dev/null +++ b/svelte/src/lib/components/ChartCanvas.svelte @@ -0,0 +1,84 @@ + + +
+
+ +
+
diff --git a/svelte/src/lib/components/ChatMessage.svelte b/svelte/src/lib/components/ChatMessage.svelte new file mode 100644 index 0000000..bb23a96 --- /dev/null +++ b/svelte/src/lib/components/ChatMessage.svelte @@ -0,0 +1,104 @@ + + + +{#each split.outside as item, oi} + {#if isToolResultBlock(item) && item.tool_use_id && toolResultMap.has(item.tool_use_id)} + + {:else if isToolUseBlock(item) && item.id && toolResultMap.has(item.id)} + + {:else} + + {/if} +{/each} + + +{#if split.chat} +
+
+
+
+ {#if isUser} + + {:else if isAssistant} + + {:else} + + {/if} +
+
+
+ +
+ {#if isUser && idx < messages.length - 1 && messages[idx + 1]?.role === 'assistant'} +
+ Delivered +
+ {/if} +
+
+
+
+ +
+ + {#if expandedRawSections[`msg-${idx}`]} +
{formatJSON(message, 0)}
+ {/if} +
+{/if} diff --git a/svelte/src/lib/components/ChatOutsideBlock.svelte b/svelte/src/lib/components/ChatOutsideBlock.svelte new file mode 100644 index 0000000..c813312 --- /dev/null +++ b/svelte/src/lib/components/ChatOutsideBlock.svelte @@ -0,0 +1,68 @@ + + +
+
+ + + {outsideLabel(item)} + +
+
+ {#if isXmlOutsideBlock(item)} + + {:else} + + {/if} +
+
+ +
+ {#if expandedRawSections[rawKey]} +
{isXmlOutsideBlock(item) ? item.raw : formatJSON(item, 0)}
+ {/if} +
+
+
diff --git a/svelte/src/lib/components/ChatRequestDetail.svelte b/svelte/src/lib/components/ChatRequestDetail.svelte new file mode 100644 index 0000000..59cc005 --- /dev/null +++ b/svelte/src/lib/components/ChatRequestDetail.svelte @@ -0,0 +1,444 @@ + + +
+ +
+
+
+
+ +
+
+
+ {ml.label} + {#if model} + {model} + {/if} +
+
+ {new Date(req.timestamp).toLocaleString()} + {#if req.response?.responseTime} + + + {(req.response.responseTime / 1000).toFixed(2)}s + + {/if} + {#if req.response?.statusCode} + {req.response.statusCode} + {/if} +
+
+
+
+ {#if req.response?.body?.usage} + {@const u = req.response.body.usage} +
+
In: {(u.input_tokens || 0).toLocaleString()}
+
Out: {(u.output_tokens || 0).toLocaleString()}
+ {#if u.cache_read_input_tokens} +
Cache: {u.cache_read_input_tokens.toLocaleString()}
+ {/if} +
+ {/if} +
+
+
+ + + {#if req.headers || req.response?.headers} +
+ + {#if expandedRawSections['headers']} +
+ + {#if req.headers && Object.keys(req.headers).length > 0} +
+
+ Request Headers + +
+ {#if expandedRawSections['headers-req-raw']} +
{formatJSON(req.headers, 0)}
+ {:else} +
+ {#each Object.entries(req.headers) as [key, values]} +
+ {key} + {Array.isArray(values) ? values.join(', ') : values} +
+ {/each} +
+ {/if} +
+ {/if} + + {#if req.response?.headers && Object.keys(req.response.headers).length > 0} +
+
+ Response Headers + +
+ {#if expandedRawSections['headers-res-raw']} +
{formatJSON(req.response.headers, 0)}
+ {:else} +
+ {#each Object.entries(req.response.headers) as [key, values]} +
+ {key} + {Array.isArray(values) ? values.join(', ') : values} +
+ {/each} +
+ {/if} +
+ {/if} +
+ {/if} +
+ {/if} + + + {#if req.body?.system && req.body.system.length > 0} +
+ + {#if expandedRawSections['system']} +
+ {#each req.body.system as sys, si} +
+
+ +
+
+ + {#if sys.cache_control} + cache: {sys.cache_control.type} + {/if} +
+ {#if expandedRawSections[`sys-raw-${si}`]} +
{formatJSON(sys, 0)}
+ {/if} +
+ {/each} +
+ {/if} +
+ {/if} + + + {#if req.body?.tools && req.body.tools.length > 0} +
+ + {#if expandedRawSections['tools']} +
+
+ {#each req.body.tools as tool} +
+ +
+ + {tool.name} +
+ {#if tool.input_schema?.properties} + {Object.keys(tool.input_schema.properties).length} params + {/if} +
+
+ {#if tool.description} + + {/if} + {#if tool.input_schema?.properties} +
+ {#each Object.entries(tool.input_schema.properties) as [name, prop]} + + {name}{#if tool.input_schema?.required?.includes(name)}*{/if} + + {/each} +
+ {/if} +
+
+ {/each} +
+
+ +
+ {#if expandedRawSections['tools-raw']} +
{formatJSON(req.body.tools, 0)}
+ {/if} +
+ {/if} +
+ {/if} + + + {#if req.body?.messages} + {@const msgs = req.body.messages} + {@const totalTurns = getTotalTurns(msgs)} + {@const trMap = buildToolResultMap(msgs)} +
+ {#each msgs as message, idx} + + {#if shouldShowTimestamp(msgs, idx)} + {@const turn = getTurnNumber(msgs, idx)} +
+ {#if idx === 0} + + {formatImessageTimestamp(req.timestamp)} + + {:else} + {@const turnReqId = getRequestIdForTurn(turn)} + {@const turnTs = getTimestampForTurn(turn)} + {@const prevTurnReqId = getRequestIdForTurn(turn - 1)} + {@const prevTurnResponseTime = prevTurnReqId ? summaryMap.get(prevTurnReqId)?.responseTime : null} +
+ {#if turnTs} + + {formatImessageTimestamp(turnTs)} + {#if prevTurnResponseTime} + · + {(prevTurnResponseTime / 1000).toFixed(1)}s + {/if} + + {/if} + {#if turnReqId && turnReqId !== selectedId} + + {:else} + + Turn {turn} of {totalTurns} + + {/if} +
+ {/if} +
+ {/if} + + + {/each} +
+ {/if} + + + {#if req.response?.completedAt} +
+ + {formatImessageTimestamp(req.response.completedAt)} + {#if req.response.responseTime} + · + {(req.response.responseTime / 1000).toFixed(1)}s + {/if} + +
+ {/if} + + + {#if req.response?.body?.content} + {@const respSplit = splitContent(req.response.body.content)} + + + {#each respSplit.outside as item, oi} + + {/each} + + + {#if respSplit.chat} +
+
+
+
+ +
+
+
+ +
+ {#if req.response.isStreaming} +
+ Streamed +
+ {/if} +
+
+
+
+ +
+ + {#if expandedRawSections['response']} +
{formatJSON(req.response.body, 0)}
+ {/if} +
+ {/if} + {/if} + + + {#if req.response?.bodyText && !req.response?.body?.content} +
+
+ + Error Response +
+
{req.response.bodyText}
+
+ {/if} + + +
+
diff --git a/svelte/src/lib/components/ChatSidebar.svelte b/svelte/src/lib/components/ChatSidebar.svelte new file mode 100644 index 0000000..5cd9552 --- /dev/null +++ b/svelte/src/lib/components/ChatSidebar.svelte @@ -0,0 +1,140 @@ + + + diff --git a/svelte/src/lib/components/ChatToolBlock.svelte b/svelte/src/lib/components/ChatToolBlock.svelte new file mode 100644 index 0000000..d5f7b52 --- /dev/null +++ b/svelte/src/lib/components/ChatToolBlock.svelte @@ -0,0 +1,182 @@ + + +
+
+ + + {item.name} + {#if summary} + {summary.length > 50 ? summary.slice(0, 50) + '\u2026' : summary} + {/if} + + {#if brief.isError} + + {brief.text} + {:else} + + {brief.text} + {/if} + +
+ {#if item.name === 'Bash'} + + {#if item.input?.description} +
{item.input.description}
+ {/if} +
+
+ $ +
{item.input?.command || ''}
+
+
+ {#if resultContent} +
+
{truncateText(resultContent, 8000)}
+
+ {/if} + {:else if item.name === 'Read'} + +
+ + {item.input?.file_path || 'file'} + {#if item.input?.offset} + L{item.input.offset}{item.input.limit ? `-${item.input.offset + item.input.limit}` : ''} + {/if} +
+ {#if resultContent} +
+
{truncateText(resultContent, 8000)}
+
+ {/if} + {:else if item.name === 'Edit'} + +
+ + {item.input?.file_path || 'file'} + {#if item.input?.replace_all} + replace all + {/if} +
+ {#if item.input?.old_string !== undefined && item.input?.new_string !== undefined} +
+
+
- old
+
{truncateText(item.input.old_string, 2000)}
+
+
+
+ new
+
{truncateText(item.input.new_string, 2000)}
+
+
+ {/if} + {#if resultContent} +
{truncateText(resultContent, 1000)}
+ {/if} + {:else if item.name === 'Write'} + +
+ + {item.input?.file_path || 'file'} + {#if item.input?.content} + {item.input.content.split('\n').length} lines + {/if} +
+ {#if item.input?.content} +
+
{truncateText(item.input.content, 5000)}
+
+ {/if} + {#if resultContent} +
{truncateText(resultContent, 500)}
+ {/if} + {:else if item.name === 'Grep'} + +
+
+ $ + rg {item.input?.pattern ? `"${item.input.pattern}"` : ''}{item.input?.glob ? ` --glob "${item.input.glob}"` : ''}{item.input?.path ? ` ${item.input.path}` : ''} +
+
+ {#if resultContent} +
+
{truncateText(resultContent, 8000)}
+
+ {/if} + {:else if item.name === 'Glob'} + +
+
+ $ + find {item.input?.pattern || ''}{item.input?.path ? ` in ${item.input.path}` : ''} +
+
+ {#if resultContent} +
+
{truncateText(resultContent, 8000)}
+
+ {/if} + {:else} + + {#if item.input && Object.keys(item.input).length > 0} +
+ {#each Object.entries(item.input) as [key, val]} +
+ {key}: +
{typeof val === 'string' ? truncateText(val, 500) : JSON.stringify(val)}
+
+ {/each} +
+ {/if} + {#if resultContent} +
+
{truncateText(resultContent, 5000)}
+
+ {/if} + {/if} + +
+ +
+ {#if expandedRawSections[rawKey]} +
{formatJSON({ tool_use: item, tool_result: result }, 0)}
+ {/if} +
+
+
diff --git a/svelte/src/lib/components/CodeDiff.svelte b/svelte/src/lib/components/CodeDiff.svelte new file mode 100644 index 0000000..e41c776 --- /dev/null +++ b/svelte/src/lib/components/CodeDiff.svelte @@ -0,0 +1,65 @@ + + +
+ {#if fileName} +
{fileName}
+ {/if} +
+ + + {#each diffLines as line, idx} + + + + + + + {/each} + +
+ {line.type === 'removed' ? '-' : line.lineNum || ''} + + {line.type === 'added' ? '+' : line.type === 'unchanged' ? line.lineNum || '' : ''} + + + {line.type === 'removed' ? '-' : line.type === 'added' ? '+' : ' '} + + + + {line.content} + +
+
+
diff --git a/svelte/src/lib/components/CodeViewer.svelte b/svelte/src/lib/components/CodeViewer.svelte new file mode 100644 index 0000000..b03ad72 --- /dev/null +++ b/svelte/src/lib/components/CodeViewer.svelte @@ -0,0 +1,169 @@ + + +{#snippet codeDisplay(inModal: boolean)} +
+
+
+ + {fileName || 'Untitled'} + {detectedLanguage} + {lineCount} lines +
+
+ + {#if !inModal} + + {/if} + +
+
+
+ + + {#each lines as line, idx (`line-${idx}`)} + + + + + {/each} + +
{idx + 1} + {#each highlightCode(line) as segment, segmentIndex (`${idx}-${segmentIndex}`)} + {#if segment.className} + {segment.text} + {:else} + {segment.text} + {/if} + {/each} +
+
+
+{/snippet} + +{@render codeDisplay(false)} + +{#if isFullscreen} + +{/if} diff --git a/svelte/src/lib/components/ConversationThread.svelte b/svelte/src/lib/components/ConversationThread.svelte new file mode 100644 index 0000000..41e073e --- /dev/null +++ b/svelte/src/lib/components/ConversationThread.svelte @@ -0,0 +1,138 @@ + + +{#if messages.length === 0} +
+
+ +
+

No messages found

+

This conversation appears to be empty

+
+{:else} +
+ +
+
toggleSection('flow')} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection('flow'); } }}> +
+
+ +
+
+

+ Conversation Flow +
+ + Conversation processed - {messages.length} messages +
+

+

{messages.length} messages • {conversation.messageCount} total

+
+
+
+ {new Date(messages[messages.length - 1]?.timestamp).toLocaleTimeString()} + {#if expandedSections.has('flow')} + + {:else} + + {/if} +
+
+
+ + + {#if expandedSections.has('flow')} +
+ {#each messages as message, index} + + {/each} + +
+
+
+
+ +
+
+
Conversation Summary
+
{messages.length} messages • {conversation.messageCount} total messages
+
+
+
+
+ + Latest: {new Date(messages[messages.length - 1]?.timestamp).toLocaleTimeString()} +
+
+
+
+
+ {/if} +
+{/if} diff --git a/svelte/src/lib/components/ImageContent.svelte b/svelte/src/lib/components/ImageContent.svelte new file mode 100644 index 0000000..3ac963f --- /dev/null +++ b/svelte/src/lib/components/ImageContent.svelte @@ -0,0 +1,90 @@ + + +{#if !imageData} +
+
+ + No image data available +
+
+{:else if imageError} +
+
+ + Failed to load image +
+
+ Show raw data +
{JSON.stringify(content, null, 2)}
+
+
+{:else} +
+
+
+ + Image ({mediaType || 'unknown type'}) +
+
+ + +
+
+
+ +
+
+ + {#if isFullscreen} + + {/if} +{/if} diff --git a/svelte/src/lib/components/MessageContent.svelte b/svelte/src/lib/components/MessageContent.svelte new file mode 100644 index 0000000..455d652 --- /dev/null +++ b/svelte/src/lib/components/MessageContent.svelte @@ -0,0 +1,226 @@ + + +{#if typeof content === 'string'} + {#if hasCustomXmlBlocks(content)} + {@const segments = parseXmlBlocks(content)} +
+ {#each segments as segment, index (`${segment.type}-${segment.tag ?? 'text'}-${index}`)} + {#if segment.type === 'xml' && segment.tag && segment.innerContent !== undefined} + + {:else if segment.type === 'text' && segment.content.trim()} +
+ +
+ {/if} + {/each} +
+ {:else} +
+ +
+ {/if} +{:else if Array.isArray(content)} +
+ {#each content as item, index (`${typeof item === 'object' && item && 'type' in item && typeof item.type === 'string' ? item.type : 'item'}-${index}`)} +
+ +
+ {/each} +
+{:else if content && typeof content === 'object'} + {#if isTextBlock(content)} + {#if content.text && content.text.includes('')} + {@const parsed = parseFunctions(content.text)} + {#if parsed} +
+ {#if parsed.beforeFunctions.trim()} +
+ +
+ {/if} +
+ +
+
+ +
+
+
+ Available Tools + +
+
{parsed.tools.length} tools defined for this conversation
+
+
+
+
+ {#each parsed.tools as toolDef, index (toolDef?.name ?? `invalid-${index}`)} + {#if toolDef} + {@const paramCount = toolDef.parameters?.properties ? Object.keys(toolDef.parameters.properties).length : 0} + {@const requiredParams = toolDef.parameters?.required || []} +
+
+
+ +
+
+ {toolDef.name} +
+ {paramCount} params + {#if requiredParams.length > 0} + {requiredParams.length} required + {/if} +
+
+
+
{toolDef.description || 'No description available'}
+
+ Show raw definition +
{JSON.stringify(toolDef, null, 2)}
+
+
+ {:else} +
+
+ + Invalid Tool Definition #{index + 1} +
+
+ {/if} + {/each} +
+
+ {#if parsed.afterFunctions.trim()} +
+ +
+ {/if} +
+ {:else} +
+ +
+ {/if} + {:else if content.text && hasCustomXmlBlocks(content.text)} + + {:else} +
+ +
+ {/if} + {:else if isToolUseBlock(content)} + + {:else if isToolResultBlock(content)} + + {:else if isImageBlock(content)} + + {:else if isThinkingBlock(content) && content.thinking && content.thinking.trim()} +
+ + + thought for {Math.ceil(content.thinking.length / 300)}s + +
{content.thinking}
+
+ {:else if isThinkingBlock(content)} + + {:else} +
+
+ + Unknown content type: {'type' in content && typeof content.type === 'string' ? content.type : 'unknown'} +
+
+ Show raw content +
{JSON.stringify(content, null, 2)}
+
+
+ {/if} +{:else} +
+
+ + Unable to render content +
+
+ Show raw content +
{JSON.stringify(content, null, 2)}
+
+
+{/if} diff --git a/svelte/src/lib/components/MessageFlow.svelte b/svelte/src/lib/components/MessageFlow.svelte new file mode 100644 index 0000000..e13eb75 --- /dev/null +++ b/svelte/src/lib/components/MessageFlow.svelte @@ -0,0 +1,182 @@ + + +
+ {#if !isLast} +
+ {/if} + +
+ {#if message.isNewInTurn} +
+ {/if} + +
+
+
+
+ {#if message.role === 'user'} + + {:else if message.role === 'system'} + + {:else} + + {/if} +
+
+ +
+
+
+ {roleConfig.name} + {#if message.isNewInTurn} + NEW + {/if} + #{index + 1} + {#if message.turnNumber} + Turn {message.turnNumber} + {/if} +
+
+ + {formatTime(message.timestamp)} +
+
+ +
+ {#if shouldShowExpander && !isExpanded} +
+
+ {#if typeof message.content === 'string'} +
{contentPreview}
+ {:else} +
+
+ {Array.isArray(message.content) ? `Message contains ${message.content.length} content blocks` : 'Complex content'} +
+ {#if Array.isArray(message.content)} +
+ {message.content.map((item) => typeof item === 'object' && item && 'type' in item && typeof item.type === 'string' ? item.type : 'unknown').join(' → ')} +
+ {/if} +
{contentPreview}
+
+ {/if} +
+ +
+ {:else} +
+ {#if shouldShowExpander && isExpanded} +
+ +
+ {/if} + +
+ {/if} +
+
+
+
+ + {#if !isLast} +
+ +
+ {/if} +
+
diff --git a/svelte/src/lib/components/Nav.svelte b/svelte/src/lib/components/Nav.svelte new file mode 100644 index 0000000..c55a028 --- /dev/null +++ b/svelte/src/lib/components/Nav.svelte @@ -0,0 +1,232 @@ + + +
+
+
+ + +
+
+
+ + +{#if showSetupModal} + + {/if} diff --git a/svelte/src/lib/components/RequestDetailContent.svelte b/svelte/src/lib/components/RequestDetailContent.svelte new file mode 100644 index 0000000..9cd886f --- /dev/null +++ b/svelte/src/lib/components/RequestDetailContent.svelte @@ -0,0 +1,687 @@ + + +
+ +
+
+

+ + Request Overview +

+
+
+
+
+ Method: + {request.method} +
+
+ Endpoint: + {getChatCompletionsEndpoint(request.routedModel, request.endpoint)} +
+
+
+
+ Timestamp: + {new Date(request.timestamp).toLocaleString()} +
+
+ User Agent: + {request.headers['User-Agent']?.[0] || 'N/A'} +
+
+
+
+ + +
+
toggleSection('headers')} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection('headers'); } }}> +
+

+ + Request Headers + {Object.keys(request.headers).length} +

+ +
+
+ {#if expandedSections.headers} +
+
+
+ + +
+ +
+ + {#if headerViewMode.request === 'pretty'} +
+ + + + + + + + + {#each Object.entries(request.headers) as [key, values]} + + + + + {/each} + +
HeaderValue
{key} + {#each values as value, i} +
0 ? 'mt-1 pt-1 border-t border-gray-100' : ''}>{value}
+ {/each} +
+
+ {:else} +
+
{formatHeadersRaw(request.headers)}
+
+ {/if} +
+ {/if} +
+ + {#if request.body} + + {#if request.body.system} +
+
toggleSection('system')} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection('system'); } }}> +
+

+ + System Instructions + {request.body.system.length} items +

+ +
+
+ {#if expandedSections.system} +
+ {#each request.body.system as sys, index} +
+
+ System Message #{index + 1} + {#if sys.cache_control} + Cache: {sys.cache_control.type} + {/if} +
+
+ +
+
+ {/each} +
+ {/if} +
+ {/if} + + + {#if request.body.tools && request.body.tools.length > 0} +
+
toggleSection('tools')} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection('tools'); } }}> +
+

+ + Available Tools + {request.body.tools.length} tools +

+ +
+
+ {#if expandedSections.tools} +
+ {#each request.body.tools as tool, index} + {@const isLongDesc = tool.description.length > 300} +
+
+
+
+
+ +
+
+
{tool.name}
+ Tool #{index + 1} +
+
+
+ {#if isLongDesc} +
+ {tool.description.slice(0, 300)}... +
{tool.description}
+
+ {:else} +
{tool.description}
+ {/if} + {#if tool.input_schema} +
+ + + Input Schema + +
+
+
{formatJSON(tool.input_schema)}
+
+
+
+ {/if} +
+
+ {/each} +
+ {/if} +
+ {/if} + + + {#if request.body.messages} +
+
toggleSection('conversation')} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection('conversation'); } }}> +
+

+ + Conversation + {request.body.messages.length} messages +

+ +
+
+ {#if expandedSections.conversation} +
+ {#each request.body.messages as message, index} + {@const roleColors: Record = { user: 'bg-blue-50 border border-blue-200', assistant: 'bg-gray-50 border border-gray-200', system: 'bg-yellow-50 border border-yellow-200' }} + {@const roleIconColors: Record = { user: 'text-blue-600', assistant: 'text-gray-600', system: 'text-yellow-600' }} +
+
+
+
+ {#if message.role === 'user'} + + {:else if message.role === 'system'} + + {:else} + + {/if} +
+ {message.role} + #{index + 1} +
+
+
+ +
+
+ {/each} +
+ {/if} +
+ {/if} + + +
+
toggleSection('model')} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection('model'); } }}> +
+

+ + Model Configuration +

+ +
+
+ {#if expandedSections.model} +
+ {#if request.routedModel && request.routedModel !== request.originalModel} +
+
+
+
+ Requested Model + {request.originalModel || request.body.model} +
+
+
+ + Routed to +
+ {request.routedModel} + {getProviderName(request.routedModel)} +
+
+
+
Target Endpoint
+ {getChatCompletionsEndpoint(request.routedModel)} +
+
+
+ {/if} +
+ {#if !request.routedModel || request.routedModel === request.originalModel} +
+
Model
+
{request.originalModel || request.body.model || 'N/A'}
+
+ {/if} +
+
Max Tokens
+
{request.body.max_tokens?.toLocaleString() || 'N/A'}
+
+
+
Temperature
+
{request.body.temperature ?? 'N/A'}
+
+
+
Stream
+
{request.body.stream ? 'Yes' : 'No'}
+
+
+
+ {/if} +
+ {/if} + + + {#if request.response} + {@const response = request.response} + {@const statusColors = getStatusColor(response.statusCode)} + {@const completedAt = response.completedAt ? new Date(response.completedAt).toLocaleString() : 'Unknown'} +
+
toggleSection('responseOverview')} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection('responseOverview'); } }}> +
+

+ + API Response + {response.statusCode} +

+ +
+
+ {#if expandedSections.responseOverview} +
+ +
+
+
Status
+
{response.statusCode}
+
+
+
Response Time
+
{response.responseTime}ms
+
+
+
Type
+
{response.isStreaming ? 'Stream' : 'Single'}
+
+
+
Completed
+
{completedAt.split(' ')[1] || 'N/A'}
+
+
+ + + {#if response.body?.usage} + {@const usage = response.body.usage} +
+
+
Input Tokens
+
{usage.input_tokens?.toLocaleString() || '0'}
+
+
+
Output Tokens
+
{usage.output_tokens?.toLocaleString() || '0'}
+
+
+
Total Tokens
+
{((usage.input_tokens || 0) + (usage.output_tokens || 0)).toLocaleString()}
+
+ {#if usage.cache_read_input_tokens} +
+
Cached Tokens
+
{usage.cache_read_input_tokens.toLocaleString()}
+
+ {/if} +
+ {/if} + + + {#if response.headers} +
+
toggleSection('responseHeaders')} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection('responseHeaders'); } }}> +
+
+ + Response Headers + {Object.keys(response.headers).length} +
+ +
+
+ {#if expandedSections.responseHeaders} +
+
+
+ + +
+ +
+ + {#if headerViewMode.response === 'pretty'} +
+ + + + + + + + + {#each Object.entries(response.headers) as [key, values]} + + + + + {/each} + +
HeaderValue
{key} + {#if Array.isArray(values)} + {#each values as value, i} +
0 ? 'mt-1 pt-1 border-t border-gray-100' : ''}>{value}
+ {/each} + {:else} + {values} + {/if} +
+
+ {:else} +
+
{formatHeadersRaw(response.headers)}
+
+ {/if} +
+ {/if} +
+ {/if} + + + {#if response.body || response.bodyText} +
+
toggleSection('responseBody')} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection('responseBody'); } }}> +
+
+ + Response Content + {#if response.body?.content && Array.isArray(response.body.content)} + {response.body.content.length} blocks + {/if} + {#if response.body?.stop_reason} + {response.body.stop_reason} + {/if} +
+ +
+
+ {#if expandedSections.responseBody} +
+ {#if response.body} + + {#if response.body.id || response.body.model} +
+ {#if response.body.id} + {response.body.id} + {/if} + {#if response.body.model} + {response.body.model} + {/if} + {#if response.body.role} + {response.body.role} + {/if} +
+ {/if} + + + {#if response.body.content && Array.isArray(response.body.content)} +
+ {#each response.body.content as block, idx} +
+
+
+ #{idx + 1} + {block.type} + {#if block.name} + {block.name} + {/if} +
+ {#if block.id} + {block.id} + {/if} +
+
+ {#if block.type === 'text' && block.text} +
+
{block.text}
+
+ {:else if block.type === 'tool_use'} +
+ {#if block.input} +
+ Tool Input +
{formatJSON(block.input)}
+
+ {/if} +
+ {:else if block.type === 'thinking' && block.thinking} +
+
+ + Thinking +
+
{block.thinking}
+
+ {:else} +
{formatJSON(block)}
+ {/if} +
+
+ {/each} +
+ {:else} + +
+ Raw Response Body +
{formatJSON(response.body)}
+
+ {/if} + + +
+ View Raw JSON +
+ +
{formatJSON(response.body)}
+
+
+ {:else if response.bodyText} +
{response.bodyText}
+ {/if} +
+ {/if} +
+ {/if} + + + {#if response.isStreaming && response.streamingChunks && response.streamingChunks.length > 0} + {@const parsed = parseStreamingResponse(response.streamingChunks)} +
+
toggleSection('streamingResponse')} onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleSection('streamingResponse'); } }}> +
+
+ + Streaming Response + {response.streamingChunks.length} chunks + {#if parsed.isFormatted} + Parsed + {/if} +
+ +
+
+ {#if expandedSections.streamingResponse} +
+ {#if parsed.isFormatted} +
+
+ + Final Response (Clean) +
+
{parsed.finalText}
+
+ {/if} +
+ Raw Streaming Data +
+
{parsed.rawData}
+
+
+
+ {/if} +
+ {/if} +
+ {/if} +
+ {/if} + + + {#if request.promptGrade} +
+

Prompt Quality Analysis

+
+
+ Overall Score: + {request.promptGrade.score}/5 +
+

{request.promptGrade.feedback}

+
+
+ {/if} +
diff --git a/svelte/src/lib/components/RichText.svelte b/svelte/src/lib/components/RichText.svelte new file mode 100644 index 0000000..dcc9395 --- /dev/null +++ b/svelte/src/lib/components/RichText.svelte @@ -0,0 +1,105 @@ + + +
+ {#each blocks as block, index (`${block.type}-${index}`)} + {#if block.type === 'paragraph'} +

+ +

+ {:else if block.type === 'heading'} + + + + {:else if block.type === 'ul' || block.type === 'ol'} + + {#each block.items as item, itemIndex (`item-${itemIndex}`)} +
  • + +
  • + {/each} +
    + {:else if block.type === 'code_block'} +
    {block.code}
    + {:else if block.type === 'hr'} +
    + {:else} +
    + {/if} + {/each} +
    diff --git a/svelte/src/lib/components/RichTextInline.svelte b/svelte/src/lib/components/RichTextInline.svelte new file mode 100644 index 0000000..fd3ac8e --- /dev/null +++ b/svelte/src/lib/components/RichTextInline.svelte @@ -0,0 +1,79 @@ + + +{#each segments as segment, index (`${segment.type}-${index}`)} + {#if segment.type === 'text'} + {segment.text} + {:else if segment.type === 'strong'} + {segment.text} + {:else if segment.type === 'em'} + {segment.text} + {:else if segment.type === 'code'} + {segment.text} + {:else} + {segment.text} + {/if} +{/each} diff --git a/svelte/src/lib/components/ThemeToggle.svelte b/svelte/src/lib/components/ThemeToggle.svelte new file mode 100644 index 0000000..e79cc86 --- /dev/null +++ b/svelte/src/lib/components/ThemeToggle.svelte @@ -0,0 +1,20 @@ + + + diff --git a/svelte/src/lib/components/TodoList.svelte b/svelte/src/lib/components/TodoList.svelte new file mode 100644 index 0000000..799f62d --- /dev/null +++ b/svelte/src/lib/components/TodoList.svelte @@ -0,0 +1,92 @@ + + +{#if !todos || todos.length === 0} +
    + +

    No tasks in the todo list

    +
    +{:else} +
    +
    +
    + + Todo List +
    +
    + {#if groupedTodos.in_progress.length > 0} + {groupedTodos.in_progress.length} in progress + {/if} + {#if groupedTodos.pending.length > 0} + {groupedTodos.pending.length} pending + {/if} + {#if groupedTodos.completed.length > 0} + {groupedTodos.completed.length} completed + {/if} +
    +
    + +
    + {#each groupedTodos.in_progress as todo} +
    +
    +

    {getTaskText(todo)}

    +
    {todo.priority}
    +
    + {/each} + {#each groupedTodos.pending as todo} +
    +
    +

    {getTaskText(todo)}

    +
    {todo.priority}
    +
    + {/each} + {#each groupedTodos.completed as todo} +
    +
    +

    {getTaskText(todo)}

    +
    {todo.priority}
    +
    + {/each} +
    +
    +{/if} diff --git a/svelte/src/lib/components/ToolResult.svelte b/svelte/src/lib/components/ToolResult.svelte new file mode 100644 index 0000000..858605a --- /dev/null +++ b/svelte/src/lib/components/ToolResult.svelte @@ -0,0 +1,182 @@ + + +
    + +
    +
    +
    +
    + {#if isError} + + {:else} + + {/if} +
    +
    +
    +
    + {config.title} + +
    + {#if toolId} +
    + + {toolId} +
    + {/if} +
    +
    + {#if isLargeContent} + + {/if} +
    + + +
    +
    +
    +
    + + Result received +
    +
    + + {isCode ? 'Code' : isJSONContent ? 'JSON' : 'Text'} + + {#if !isCode} + {displayContent.length} chars + {/if} +
    +
    + + {#if isCode} + + {:else if isJSONContent} +
    {truncatedContent}
    + {:else} +
    {truncatedContent}
    + {/if} + + {#if shouldTruncate && !isCode} +
    + +
    + {/if} +
    +
    + + + {#if content && typeof content === 'object' && Object.keys(content).length > 1} +
    +
    + + + Show raw data structure + +
    +
    {formatJSON(content)}
    +
    +
    +
    + {/if} + + +
    +
    +
    + {config.statusText} +
    +
    +
    diff --git a/svelte/src/lib/components/ToolUse.svelte b/svelte/src/lib/components/ToolUse.svelte new file mode 100644 index 0000000..2fb1fc1 --- /dev/null +++ b/svelte/src/lib/components/ToolUse.svelte @@ -0,0 +1,173 @@ + + + +
    + +
    +
    +
    + +
    +
    +
    + Tool Execution + +
    +
    + + {name} +
    +
    +
    +
    + {id} + +
    +
    + + + {#if name === 'Edit' && input.old_string && input.new_string} +
    +
    Code Changes
    + +
    + {/if} + + + {#if name === 'Read' && input.file_path} +
    +
    File Contents
    +
    Reading: {input.file_path}
    +
    + {/if} + + + {#if name === 'TodoWrite' && input.todos && Array.isArray(input.todos)} +
    +
    Task Management
    + +
    + {/if} + + + {#if inputKeys.length > 0} +
    +
    +
    + Parameters + {inputKeys.length} +
    + {#if inputKeys.length > 2} + + {/if} +
    + + {#if name !== 'Edit' && name !== 'TodoWrite'} +
    +
    + {#each Object.entries(input) as [key, value] (key)} +
    + {key}: +
    + {#if typeof value === 'string'} + {#if value.length > 200 || value.includes('\n')} +
    + Show large parameter +
    {value}
    +
    + {:else} + {value} + {/if} + {:else if Array.isArray(value)} +
    + Show array ({value.length} items) +
    {formatJSON(value)}
    +
    + {:else if isComplexObject(value)} +
    + Show object ({objectKeyCount(value)} properties) +
    {formatJSON(value)}
    +
    + {:else} + {formatValue(value)} + {/if} +
    +
    + {/each} +
    + + {#if !isParamsExpanded && inputKeys.length > 2} +
    + +
    + {/if} +
    + {/if} +
    + {/if} + + + {#if text} +
    +
    Additional Information:
    +
    {text}
    +
    + {/if} + + +
    +
    +
    + Tool execution initiated +
    +
    +
    diff --git a/svelte/src/lib/components/XmlBlock.svelte b/svelte/src/lib/components/XmlBlock.svelte new file mode 100644 index 0000000..96b1fb8 --- /dev/null +++ b/svelte/src/lib/components/XmlBlock.svelte @@ -0,0 +1,87 @@ + + +
    + + + {#if isExpanded} +
    + {#if hasNestedXml} +
    + {#each innerSegments as segment, index (`${segment.type}-${segment.tag ?? 'text'}-${index}`)} + {#if segment.type === 'xml' && segment.tag && segment.innerContent !== undefined} + + {:else if segment.type === 'text' && segment.content.trim()} + + {/if} + {/each} +
    + {:else} +
    {innerContent.trim()}
    + {/if} +
    + {/if} +
    diff --git a/svelte/src/lib/formatters.ts b/svelte/src/lib/formatters.ts new file mode 100644 index 0000000..31fbada --- /dev/null +++ b/svelte/src/lib/formatters.ts @@ -0,0 +1 @@ +export * from '../../../shared/frontend/formatters'; diff --git a/svelte/src/lib/models.ts b/svelte/src/lib/models.ts new file mode 100644 index 0000000..36ade49 --- /dev/null +++ b/svelte/src/lib/models.ts @@ -0,0 +1,29 @@ +/** + * Utility functions for model-related operations + */ + +export function isOpenAIModel(model: string | null | undefined): boolean { + if (!model) return false; + return model.startsWith('gpt-') || /^o[0-9]/.test(model); +} + +export function getProviderName(model: string | null | undefined): 'OpenAI' | 'Anthropic' { + return isOpenAIModel(model) ? 'OpenAI' : 'Anthropic'; +} + +export function getChatCompletionsEndpoint(model: string | null | undefined, defaultEndpoint?: string): string { + return isOpenAIModel(model) ? '/v1/chat/completions' : (defaultEndpoint || '/v1/messages'); +} + +/** + * Get a short display label and color class for a model string. + */ +export function getModelDisplay(model: string): { label: string; colorClass: string } { + if (!model) return { label: 'API', colorClass: 'text-gray-900' }; + const m = model.toLowerCase(); + if (m.includes('opus')) return { label: 'Opus', colorClass: 'text-purple-600' }; + if (m.includes('sonnet')) return { label: 'Sonnet', colorClass: 'text-indigo-600' }; + if (m.includes('haiku')) return { label: 'Haiku', colorClass: 'text-teal-600' }; + if (isOpenAIModel(model)) return { label: model.includes('gpt-4o') ? 'GPT-4o' : model.split('-')[0].toUpperCase(), colorClass: 'text-green-600' }; + return { label: model.split('-')[0], colorClass: 'text-gray-900' }; +} diff --git a/svelte/src/lib/pricing.ts b/svelte/src/lib/pricing.ts new file mode 100644 index 0000000..969422a --- /dev/null +++ b/svelte/src/lib/pricing.ts @@ -0,0 +1,118 @@ +import { isOpenAIModel } from './models'; + +/** + * Anthropic API pricing (per million tokens) as of March 2026 + * https://docs.anthropic.com/en/docs/about-claude/pricing + */ + +export interface ModelPricing { + inputPerMTok: number; + outputPerMTok: number; + cacheReadPerMTok: number; + cacheWritePerMTok: number; + label: string; + tier: 'opus' | 'sonnet' | 'haiku' | 'unknown'; +} + +const PRICING: Record = { + // Opus 4 family + 'claude-opus-4': { inputPerMTok: 15, outputPerMTok: 75, cacheReadPerMTok: 1.50, cacheWritePerMTok: 18.75, label: 'Opus 4', tier: 'opus' }, + // Sonnet 4 family + 'claude-sonnet-4': { inputPerMTok: 3, outputPerMTok: 15, cacheReadPerMTok: 0.30, cacheWritePerMTok: 3.75, label: 'Sonnet 4', tier: 'sonnet' }, + // Haiku 3.5 + 'claude-haiku-3': { inputPerMTok: 0.80, outputPerMTok: 4, cacheReadPerMTok: 0.08, cacheWritePerMTok: 1, label: 'Haiku 3.5', tier: 'haiku' }, +}; + +// Subscription plans for comparison +export interface SubscriptionPlan { + name: string; + monthlyPrice: number; + description: string; +} + +export const SUBSCRIPTION_PLANS: SubscriptionPlan[] = [ + { name: 'Claude Pro', monthlyPrice: 20, description: 'Standard usage limits' }, + { name: 'Claude Max 5x', monthlyPrice: 100, description: '5x Pro usage' }, + { name: 'Claude Max 20x', monthlyPrice: 200, description: '20x Pro usage' }, +]; + +/** + * Match a model string to its pricing tier + */ +export function getModelPricing(model: string): ModelPricing { + if (isOpenAIModel(model)) { + return { ...PRICING['claude-sonnet-4'], label: model.split('-').slice(0, 2).join('-'), tier: 'unknown' }; + } + const m = model.toLowerCase(); + if (m.includes('opus')) return PRICING['claude-opus-4']; + if (m.includes('sonnet')) return PRICING['claude-sonnet-4']; + if (m.includes('haiku')) return PRICING['claude-haiku-3']; + // Default to sonnet pricing for unknown models + return { ...PRICING['claude-sonnet-4'], label: model.split('-').slice(0, 2).join('-'), tier: 'unknown' }; +} + +/** + * Calculate cost for a given token count and model + */ +export function calculateCost( + inputTokens: number, + outputTokens: number, + cacheReadTokens: number, + cacheWriteTokens: number, + model: string +): number { + const pricing = getModelPricing(model); + return ( + (inputTokens / 1_000_000) * pricing.inputPerMTok + + (outputTokens / 1_000_000) * pricing.outputPerMTok + + (cacheReadTokens / 1_000_000) * pricing.cacheReadPerMTok + + (cacheWriteTokens / 1_000_000) * pricing.cacheWritePerMTok + ); +} + +/** + * Calculate costs from usage stats broken down by model + */ +export function calculateTotalCostFromStats( + requestsByModel: Record +): { totalCost: number; costByModel: Array<{ model: string; label: string; cost: number; inputTokens: number; outputTokens: number; cacheTokens: number; requests: number }> } { + let totalCost = 0; + const costByModel: Array<{ model: string; label: string; cost: number; inputTokens: number; outputTokens: number; cacheTokens: number; requests: number }> = []; + + for (const [model, stats] of Object.entries(requestsByModel)) { + const pricing = getModelPricing(model); + // Treat cache_tokens as cache reads (most common case) + const cost = calculateCost(stats.input_tokens, stats.output_tokens, stats.cache_tokens, 0, model); + totalCost += cost; + costByModel.push({ + model, + label: pricing.label, + cost, + inputTokens: stats.input_tokens, + outputTokens: stats.output_tokens, + cacheTokens: stats.cache_tokens, + requests: stats.request_count, + }); + } + + // Sort by cost descending + costByModel.sort((a, b) => b.cost - a.cost); + + return { totalCost, costByModel }; +} + +/** + * Format a dollar amount for display + */ +export function formatCost(cost: number): string { + if (cost < 0.01) return `$${cost.toFixed(4)}`; + if (cost < 1) return `$${cost.toFixed(3)}`; + return `$${cost.toFixed(2)}`; +} + +/** + * Project monthly cost from a daily rate + */ +export function projectMonthlyCost(dailyCost: number): number { + return dailyCost * 30; +} diff --git a/svelte/src/lib/rich-text.ts b/svelte/src/lib/rich-text.ts new file mode 100644 index 0000000..91d8df6 --- /dev/null +++ b/svelte/src/lib/rich-text.ts @@ -0,0 +1,170 @@ +export type RichTextInline = + | { type: 'text'; text: string } + | { type: 'strong'; text: string } + | { type: 'em'; text: string } + | { type: 'code'; text: string } + | { type: 'link'; text: string; href: string }; + +export type RichTextBlock = + | { type: 'paragraph'; content: RichTextInline[] } + | { type: 'heading'; level: number; content: RichTextInline[] } + | { type: 'ul'; items: RichTextInline[][] } + | { type: 'ol'; items: RichTextInline[][] } + | { type: 'code_block'; code: string } + | { type: 'hr' } + | { type: 'spacer' }; + +const INLINE_TOKEN_PATTERN = /(https?:\/\/[^\s<]+|`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*)/g; + +function pushText(segments: RichTextInline[], text: string) { + if (!text) return; + segments.push({ type: 'text', text }); +} + +function splitTrailingPunctuation(url: string): { href: string; trailing: string } { + const trailingMatch = url.match(/[),.!?;:]+$/); + if (!trailingMatch) return { href: url, trailing: '' }; + + const trailing = trailingMatch[0]; + return { + href: url.slice(0, url.length - trailing.length), + trailing + }; +} + +export function parseInlineRichText(text: string): RichTextInline[] { + if (!text) return []; + + const segments: RichTextInline[] = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = INLINE_TOKEN_PATTERN.exec(text)) !== null) { + if (match.index > lastIndex) { + pushText(segments, text.slice(lastIndex, match.index)); + } + + const token = match[0]; + if (token.startsWith('**') && token.endsWith('**')) { + segments.push({ type: 'strong', text: token.slice(2, -2) }); + } else if (token.startsWith('*') && token.endsWith('*')) { + segments.push({ type: 'em', text: token.slice(1, -1) }); + } else if (token.startsWith('`') && token.endsWith('`')) { + segments.push({ type: 'code', text: token.slice(1, -1) }); + } else { + const { href, trailing } = splitTrailingPunctuation(token); + segments.push({ type: 'link', text: href, href }); + pushText(segments, trailing); + } + + lastIndex = match.index + token.length; + } + + if (lastIndex < text.length) { + pushText(segments, text.slice(lastIndex)); + } + + return segments; +} + +export function parseRichText(text: string): RichTextBlock[] { + if (!text) return []; + + const lines = text.split('\n'); + const blocks: RichTextBlock[] = []; + let index = 0; + let activeListType: 'ul' | 'ol' | null = null; + let activeListItems: RichTextInline[][] = []; + + function flushList() { + if (!activeListType || activeListItems.length === 0) return; + blocks.push({ + type: activeListType, + items: activeListItems + }); + activeListType = null; + activeListItems = []; + } + + while (index < lines.length) { + const line = lines[index]; + const trimmed = line.trim(); + + if (/^```/.test(trimmed)) { + flushList(); + const codeLines: string[] = []; + index += 1; + while (index < lines.length && !/^```/.test(lines[index].trim())) { + codeLines.push(lines[index]); + index += 1; + } + blocks.push({ type: 'code_block', code: codeLines.join('\n') }); + if (index < lines.length) index += 1; + continue; + } + + if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) { + flushList(); + blocks.push({ type: 'hr' }); + index += 1; + continue; + } + + const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); + if (headingMatch) { + flushList(); + blocks.push({ + type: 'heading', + level: headingMatch[1].length, + content: parseInlineRichText(headingMatch[2]) + }); + index += 1; + continue; + } + + const bulletMatch = line.match(/^\s*[-*+]\s+(.+)$/); + if (bulletMatch) { + if (activeListType !== 'ul') { + flushList(); + activeListType = 'ul'; + } + activeListItems.push(parseInlineRichText(bulletMatch[1])); + index += 1; + continue; + } + + const numberMatch = line.match(/^\s*\d+[.)]\s+(.+)$/); + if (numberMatch) { + if (activeListType !== 'ol') { + flushList(); + activeListType = 'ol'; + } + activeListItems.push(parseInlineRichText(numberMatch[1])); + index += 1; + continue; + } + + if (trimmed === '') { + flushList(); + if (blocks[blocks.length - 1]?.type !== 'spacer') { + blocks.push({ type: 'spacer' }); + } + index += 1; + continue; + } + + flushList(); + blocks.push({ + type: 'paragraph', + content: parseInlineRichText(line) + }); + index += 1; + } + + flushList(); + + while (blocks[0]?.type === 'spacer') blocks.shift(); + while (blocks[blocks.length - 1]?.type === 'spacer') blocks.pop(); + + return blocks; +} diff --git a/svelte/src/lib/theme.svelte.ts b/svelte/src/lib/theme.svelte.ts new file mode 100644 index 0000000..71f5c42 --- /dev/null +++ b/svelte/src/lib/theme.svelte.ts @@ -0,0 +1,45 @@ +export type ThemeMode = 'light' | 'dark' | 'system'; + +let mode = $state('system'); + +function getSystemPreference(): boolean { + if (typeof window === 'undefined') return false; + return window.matchMedia('(prefers-color-scheme: dark)').matches; +} + +function applyTheme(m: ThemeMode) { + if (typeof document === 'undefined') return; + const isDark = m === 'dark' || (m === 'system' && getSystemPreference()); + document.documentElement.classList.toggle('dark', isDark); +} + +export function initTheme() { + if (typeof window === 'undefined') return; + const stored = localStorage.getItem('theme') as ThemeMode | null; + if (stored && ['light', 'dark', 'system'].includes(stored)) { + mode = stored; + } + applyTheme(mode); + + // Listen for system theme changes + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + mq.addEventListener('change', () => { + if (mode === 'system') applyTheme('system'); + }); +} + +export function getTheme(): ThemeMode { + return mode; +} + +export function setTheme(m: ThemeMode) { + mode = m; + localStorage.setItem('theme', m); + applyTheme(m); +} + +export function cycleTheme() { + const order: ThemeMode[] = ['system', 'light', 'dark']; + const next = order[(order.indexOf(mode) + 1) % order.length]; + setTheme(next); +} diff --git a/svelte/src/lib/types.ts b/svelte/src/lib/types.ts new file mode 100644 index 0000000..f3cb4a5 --- /dev/null +++ b/svelte/src/lib/types.ts @@ -0,0 +1 @@ +export * from '../../../shared/frontend/types'; diff --git a/svelte/src/routes/+error.svelte b/svelte/src/routes/+error.svelte new file mode 100644 index 0000000..bcbe348 --- /dev/null +++ b/svelte/src/routes/+error.svelte @@ -0,0 +1,19 @@ + + +
    +
    + +

    {$page.status}

    +

    {$page.error?.message}

    + +
    +
    diff --git a/svelte/src/routes/+layout.server.ts b/svelte/src/routes/+layout.server.ts new file mode 100644 index 0000000..ca661b1 --- /dev/null +++ b/svelte/src/routes/+layout.server.ts @@ -0,0 +1,25 @@ +import type { ServerLoadEvent } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; + +export function load({ request }: ServerLoadEvent) { + // Explicit override takes priority + if (env.PROXY_PUBLIC_URL) { + return { proxyUrl: env.PROXY_PUBLIC_URL.replace(/\/$/, '') }; + } + + // Derive from reverse-proxy forwarded headers (Traefik sets these) + const forwardedProto = request.headers.get('x-forwarded-proto') || 'https'; + const forwardedHost = request.headers.get('x-forwarded-host') || ''; + + if (forwardedHost) { + const proxyHost = forwardedHost + .replace(/^claude-code-proxy-svelte\./, 'claude-code-proxy.') + .replace(/^claude-code-proxy-web\./, 'claude-code-proxy.'); + return { proxyUrl: `${forwardedProto}://${proxyHost}` }; + } + + // Local dev fallback + const host = request.headers.get('host') || 'localhost:3001'; + const hostname = host.split(':')[0]; + return { proxyUrl: `http://${hostname}:3001` }; +} diff --git a/svelte/src/routes/+layout.svelte b/svelte/src/routes/+layout.svelte new file mode 100644 index 0000000..32e55e1 --- /dev/null +++ b/svelte/src/routes/+layout.svelte @@ -0,0 +1,18 @@ + + + + Claude Code Proxy + + + +{@render children()} diff --git a/svelte/src/routes/+page.server.ts b/svelte/src/routes/+page.server.ts new file mode 100644 index 0000000..2941953 --- /dev/null +++ b/svelte/src/routes/+page.server.ts @@ -0,0 +1 @@ +// proxyUrl is now provided by +layout.server.ts diff --git a/svelte/src/routes/+page.svelte b/svelte/src/routes/+page.svelte new file mode 100644 index 0000000..226179f --- /dev/null +++ b/svelte/src/routes/+page.svelte @@ -0,0 +1,318 @@ + + +
    +
    diff --git a/svelte/src/routes/analytics/+page.svelte b/svelte/src/routes/analytics/+page.svelte new file mode 100644 index 0000000..70880d6 --- /dev/null +++ b/svelte/src/routes/analytics/+page.svelte @@ -0,0 +1,980 @@ + + + + Analytics - Claude Code Proxy + + +
    +
    diff --git a/svelte/src/routes/api/conversations/+server.ts b/svelte/src/routes/api/conversations/+server.ts new file mode 100644 index 0000000..e218547 --- /dev/null +++ b/svelte/src/routes/api/conversations/+server.ts @@ -0,0 +1,13 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async ({ url }) => { + const res = await fetch(buildBackendURL('/api/conversations', url.searchParams), { + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/conversations/[id]/+server.ts b/svelte/src/routes/api/conversations/[id]/+server.ts new file mode 100644 index 0000000..18db68f --- /dev/null +++ b/svelte/src/routes/api/conversations/[id]/+server.ts @@ -0,0 +1,14 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async ({ params, url }) => { + const res = await fetch( + buildBackendURL(`/api/conversations/${encodeURIComponent(params.id)}`, url.searchParams), + { headers: backendAuthHeaders() } + ); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/grade-prompt/+server.ts b/svelte/src/routes/api/grade-prompt/+server.ts new file mode 100644 index 0000000..e364b2e --- /dev/null +++ b/svelte/src/routes/api/grade-prompt/+server.ts @@ -0,0 +1,19 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const POST: RequestHandler = async ({ request }) => { + const body = await request.text(); + const res = await fetch(buildBackendURL('/api/grade-prompt'), { + method: 'POST', + headers: { + 'content-type': 'application/json', + ...backendAuthHeaders() + }, + body + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/requests/+server.ts b/svelte/src/routes/api/requests/+server.ts new file mode 100644 index 0000000..81594ea --- /dev/null +++ b/svelte/src/routes/api/requests/+server.ts @@ -0,0 +1,24 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async ({ url }) => { + const res = await fetch(buildBackendURL('/api/requests', url.searchParams), { + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; + +export const DELETE: RequestHandler = async () => { + const res = await fetch(buildBackendURL('/api/requests'), { + method: 'DELETE', + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/requests/[id]/+server.ts b/svelte/src/routes/api/requests/[id]/+server.ts new file mode 100644 index 0000000..fd8930c --- /dev/null +++ b/svelte/src/routes/api/requests/[id]/+server.ts @@ -0,0 +1,13 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async ({ params }) => { + const res = await fetch(buildBackendURL(`/api/requests/${encodeURIComponent(params.id)}`), { + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/requests/latest-date/+server.ts b/svelte/src/routes/api/requests/latest-date/+server.ts new file mode 100644 index 0000000..0d3f819 --- /dev/null +++ b/svelte/src/routes/api/requests/latest-date/+server.ts @@ -0,0 +1,13 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async () => { + const res = await fetch(buildBackendURL('/api/requests/latest-date'), { + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/requests/summary/+server.ts b/svelte/src/routes/api/requests/summary/+server.ts new file mode 100644 index 0000000..c1bae9f --- /dev/null +++ b/svelte/src/routes/api/requests/summary/+server.ts @@ -0,0 +1,13 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async ({ url }) => { + const res = await fetch(buildBackendURL('/api/requests/summary', url.searchParams), { + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/settings/+server.ts b/svelte/src/routes/api/settings/+server.ts new file mode 100644 index 0000000..13e0d98 --- /dev/null +++ b/svelte/src/routes/api/settings/+server.ts @@ -0,0 +1,25 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async () => { + const res = await fetch(buildBackendURL('/api/settings'), { + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; + +export const PUT: RequestHandler = async ({ request }) => { + const res = await fetch(buildBackendURL('/api/settings'), { + method: 'PUT', + headers: { ...backendAuthHeaders(), 'content-type': 'application/json' }, + body: await request.text() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/stats/+server.ts b/svelte/src/routes/api/stats/+server.ts new file mode 100644 index 0000000..aa467bf --- /dev/null +++ b/svelte/src/routes/api/stats/+server.ts @@ -0,0 +1,13 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async ({ url }) => { + const res = await fetch(buildBackendURL('/api/stats', url.searchParams), { + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/stats/dashboard/+server.ts b/svelte/src/routes/api/stats/dashboard/+server.ts new file mode 100644 index 0000000..1659988 --- /dev/null +++ b/svelte/src/routes/api/stats/dashboard/+server.ts @@ -0,0 +1,13 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async ({ url }) => { + const res = await fetch(buildBackendURL('/api/stats/dashboard', url.searchParams), { + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/stats/hourly/+server.ts b/svelte/src/routes/api/stats/hourly/+server.ts new file mode 100644 index 0000000..1d30950 --- /dev/null +++ b/svelte/src/routes/api/stats/hourly/+server.ts @@ -0,0 +1,13 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async ({ url }) => { + const res = await fetch(buildBackendURL('/api/stats/hourly', url.searchParams), { + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/stats/models/+server.ts b/svelte/src/routes/api/stats/models/+server.ts new file mode 100644 index 0000000..d685837 --- /dev/null +++ b/svelte/src/routes/api/stats/models/+server.ts @@ -0,0 +1,13 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async ({ url }) => { + const res = await fetch(buildBackendURL('/api/stats/models', url.searchParams), { + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/api/stats/organizations/+server.ts b/svelte/src/routes/api/stats/organizations/+server.ts new file mode 100644 index 0000000..b9d595f --- /dev/null +++ b/svelte/src/routes/api/stats/organizations/+server.ts @@ -0,0 +1,13 @@ +import type { RequestHandler } from './$types'; +import { backendAuthHeaders } from '$lib/auth.server'; +import { buildBackendURL } from '$lib/backend.server'; + +export const GET: RequestHandler = async ({ url }) => { + const res = await fetch(buildBackendURL('/api/stats/organizations', url.searchParams), { + headers: backendAuthHeaders() + }); + return new Response(res.body, { + status: res.status, + headers: { 'content-type': res.headers.get('content-type') || 'application/json' } + }); +}; diff --git a/svelte/src/routes/chat/+page.svelte b/svelte/src/routes/chat/+page.svelte new file mode 100644 index 0000000..55aeeec --- /dev/null +++ b/svelte/src/routes/chat/+page.svelte @@ -0,0 +1,217 @@ + + +
    +
    diff --git a/svelte/src/routes/conversations/+page.svelte b/svelte/src/routes/conversations/+page.svelte new file mode 100644 index 0000000..080ff9c --- /dev/null +++ b/svelte/src/routes/conversations/+page.svelte @@ -0,0 +1,194 @@ + + + + Conversations - Claude Code Proxy + + +
    +
    diff --git a/svelte/src/routes/settings/+page.svelte b/svelte/src/routes/settings/+page.svelte new file mode 100644 index 0000000..fa610b5 --- /dev/null +++ b/svelte/src/routes/settings/+page.svelte @@ -0,0 +1,274 @@ + + +
    +
    diff --git a/web/public/favicon.ico b/svelte/static/favicon.ico similarity index 100% rename from web/public/favicon.ico rename to svelte/static/favicon.ico diff --git a/svelte/static/fonts/inter-latin-ext.woff2 b/svelte/static/fonts/inter-latin-ext.woff2 new file mode 100644 index 0000000..03aaea1 Binary files /dev/null and b/svelte/static/fonts/inter-latin-ext.woff2 differ diff --git a/svelte/static/fonts/inter-latin.woff2 b/svelte/static/fonts/inter-latin.woff2 new file mode 100644 index 0000000..33002f1 Binary files /dev/null and b/svelte/static/fonts/inter-latin.woff2 differ diff --git a/svelte/svelte.config.js b/svelte/svelte.config.js new file mode 100644 index 0000000..b4b7de8 --- /dev/null +++ b/svelte/svelte.config.js @@ -0,0 +1,12 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter() + } +}; + +export default config; diff --git a/svelte/tailwind.config.ts b/svelte/tailwind.config.ts new file mode 100644 index 0000000..e78bde3 --- /dev/null +++ b/svelte/tailwind.config.ts @@ -0,0 +1,23 @@ +import type { Config } from 'tailwindcss'; + +export default { + darkMode: 'class', + content: ['./src/**/*.{html,js,svelte,ts}'], + theme: { + extend: { + fontFamily: { + sans: [ + 'Inter', + 'ui-sans-serif', + 'system-ui', + 'sans-serif', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji' + ] + } + } + }, + plugins: [] +} satisfies Config; diff --git a/svelte/tsconfig.json b/svelte/tsconfig.json new file mode 100644 index 0000000..a8f10c8 --- /dev/null +++ b/svelte/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/svelte/vite.config.ts b/svelte/vite.config.ts new file mode 100644 index 0000000..08cae56 --- /dev/null +++ b/svelte/vite.config.ts @@ -0,0 +1,32 @@ +import path from 'node:path'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig, loadEnv, searchForWorkspaceRoot } from 'vite'; +import { resolveBackendOrigin } from '../shared/frontend/backend'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); + + return { + plugins: [sveltekit()], + server: { + allowedHosts: true, + fs: { + allow: [ + searchForWorkspaceRoot(process.cwd()), + path.resolve(__dirname, '../shared') + ] + }, + hmr: { + // When behind a reverse proxy (Traefik), use the client's host for WebSocket + clientPort: 443, + protocol: 'wss' + }, + proxy: { + '/api': { + target: resolveBackendOrigin(env), + changeOrigin: true + } + } + } + }; +}); diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs deleted file mode 100644 index 4f6f59e..0000000 --- a/web/.eslintrc.cjs +++ /dev/null @@ -1,84 +0,0 @@ -/** - * This is intended to be a basic starting point for linting in your app. - * It relies on recommended configs out of the box for simplicity, but you can - * and should modify this configuration to best suit your team's needs. - */ - -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { - jsx: true, - }, - }, - env: { - browser: true, - commonjs: true, - es6: true, - }, - ignorePatterns: ["!**/.server", "!**/.client"], - - // Base config - extends: ["eslint:recommended"], - - overrides: [ - // React - { - files: ["**/*.{js,jsx,ts,tsx}"], - plugins: ["react", "jsx-a11y"], - extends: [ - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "plugin:jsx-a11y/recommended", - ], - settings: { - react: { - version: "detect", - }, - formComponents: ["Form"], - linkComponents: [ - { name: "Link", linkAttribute: "to" }, - { name: "NavLink", linkAttribute: "to" }, - ], - "import/resolver": { - typescript: {}, - }, - }, - }, - - // Typescript - { - files: ["**/*.{ts,tsx}"], - plugins: ["@typescript-eslint", "import"], - parser: "@typescript-eslint/parser", - settings: { - "import/internal-regex": "^~/", - "import/resolver": { - node: { - extensions: [".ts", ".tsx"], - }, - typescript: { - alwaysTryTypes: true, - }, - }, - }, - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], - }, - - // Node - { - files: [".eslintrc.cjs"], - env: { - node: true, - }, - }, - ], -}; diff --git a/web/app/components/CodeDiff.tsx b/web/app/components/CodeDiff.tsx deleted file mode 100644 index 9b950ba..0000000 --- a/web/app/components/CodeDiff.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react'; - -interface CodeDiffProps { - oldCode: string; - newCode: string; - fileName?: string; -} - -export function CodeDiff({ oldCode, newCode, fileName }: CodeDiffProps) { - // Split code into lines - const oldLines = oldCode.split('\n'); - const newLines = newCode.split('\n'); - - // Simple diff algorithm - find common prefix and suffix - let start = 0; - let oldEnd = oldLines.length - 1; - let newEnd = newLines.length - 1; - - // Find common prefix - while (start <= oldEnd && start <= newEnd && oldLines[start] === newLines[start]) { - start++; - } - - // Find common suffix - while (oldEnd >= start && newEnd >= start && oldLines[oldEnd] === newLines[newEnd]) { - oldEnd--; - newEnd--; - } - - // Build diff display - const diffLines: Array<{ type: 'unchanged' | 'removed' | 'added'; content: string; lineNum?: number }> = []; - - // Add unchanged prefix - for (let i = 0; i < start; i++) { - diffLines.push({ type: 'unchanged', content: oldLines[i], lineNum: i + 1 }); - } - - // Add removed lines - for (let i = start; i <= oldEnd; i++) { - diffLines.push({ type: 'removed', content: oldLines[i] }); - } - - // Add added lines - for (let i = start; i <= newEnd; i++) { - diffLines.push({ type: 'added', content: newLines[i], lineNum: i + 1 }); - } - - // Add unchanged suffix - for (let i = oldEnd + 1; i < oldLines.length; i++) { - diffLines.push({ type: 'unchanged', content: oldLines[i], lineNum: i + 1 + (newEnd - oldEnd) }); - } - - return ( -
    - {fileName && ( -
    - {fileName} -
    - )} -
    - - - {diffLines.map((line, idx) => ( - - - - - - - ))} - -
    - {line.type === 'removed' ? '-' : line.lineNum || ''} - - {line.type === 'added' ? '+' : line.type === 'unchanged' ? line.lineNum || '' : ''} - - - {line.type === 'removed' ? '-' : line.type === 'added' ? '+' : ' '} - - - - {line.content} - -
    -
    -
    - ); -} \ No newline at end of file diff --git a/web/app/components/CodeViewer.tsx b/web/app/components/CodeViewer.tsx deleted file mode 100644 index f9f4aae..0000000 --- a/web/app/components/CodeViewer.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { useState } from 'react'; -import { Copy, Check, FileCode, Download, Maximize2, X } from 'lucide-react'; - -interface CodeViewerProps { - code: string; - fileName?: string; - language?: string; -} - -export function CodeViewer({ code, fileName, language }: CodeViewerProps) { - const [copied, setCopied] = useState(false); - const [isFullscreen, setIsFullscreen] = useState(false); - - // Determine language from file extension - const getLanguageFromFileName = (filename?: string): string => { - if (!filename) return 'text'; - - const extension = filename.split('.').pop()?.toLowerCase(); - const languageMap: Record = { - 'js': 'javascript', - 'jsx': 'javascript', - 'ts': 'typescript', - 'tsx': 'typescript', - 'py': 'python', - 'rb': 'ruby', - 'go': 'go', - 'rs': 'rust', - 'java': 'java', - 'cpp': 'cpp', - 'c': 'c', - 'h': 'c', - 'hpp': 'cpp', - 'cs': 'csharp', - 'php': 'php', - 'swift': 'swift', - 'kt': 'kotlin', - 'scala': 'scala', - 'r': 'r', - 'sh': 'bash', - 'bash': 'bash', - 'zsh': 'bash', - 'fish': 'bash', - 'ps1': 'powershell', - 'sql': 'sql', - 'html': 'html', - 'htm': 'html', - 'xml': 'xml', - 'css': 'css', - 'scss': 'scss', - 'sass': 'sass', - 'less': 'less', - 'json': 'json', - 'yaml': 'yaml', - 'yml': 'yaml', - 'toml': 'toml', - 'md': 'markdown', - 'mdx': 'markdown', - 'tex': 'latex', - 'dockerfile': 'dockerfile', - 'makefile': 'makefile', - 'cmake': 'cmake', - 'gradle': 'gradle', - 'maven': 'xml', - 'vim': 'vim', - 'lua': 'lua', - 'dart': 'dart', - 'elixir': 'elixir', - 'elm': 'elm', - 'erlang': 'erlang', - 'haskell': 'haskell', - 'julia': 'julia', - 'nim': 'nim', - 'perl': 'perl', - 'ocaml': 'ocaml', - 'clj': 'clojure', - 'cljs': 'clojure', - 'cljc': 'clojure' - }; - - return languageMap[extension || ''] || 'text'; - }; - - const detectedLanguage = language || getLanguageFromFileName(fileName); - - // Basic syntax highlighting for common tokens - const highlightCode = (code: string): string => { - // Escape HTML - let highlighted = code - .replace(/&/g, '&') - .replace(//g, '>'); - - // Common patterns for many languages - const patterns = [ - // Strings - { regex: /(["'`])(?:(?=(\\?))\2.)*?\1/g, class: 'text-green-400' }, - // Comments - { regex: /(\/\/.*$)/gm, class: 'text-gray-500 italic' }, - { regex: /(\/\*[\s\S]*?\*\/)/g, class: 'text-gray-500 italic' }, - { regex: /(#.*$)/gm, class: 'text-gray-500 italic' }, - // Numbers - { regex: /\b(\d+\.?\d*)\b/g, class: 'text-purple-400' }, - // Keywords (common across many languages) - { regex: /\b(function|const|let|var|if|else|for|while|return|class|import|export|from|async|await|def|elif|except|finally|lambda|with|as|raise|del|global|nonlocal|assert|break|continue|try|catch|throw|new|this|super|extends|implements|interface|abstract|static|public|private|protected|void|int|string|boolean|float|double|char|long|short|byte|enum|struct|typedef|union|namespace|using|package|goto|switch|case|default)\b/g, class: 'text-blue-400' }, - // Boolean and null values - { regex: /\b(true|false|null|undefined|nil|None|True|False)\b/g, class: 'text-orange-400' }, - // Function calls (basic) - { regex: /(\w+)(?=\s*\()/g, class: 'text-yellow-400' }, - // Types/Classes (PascalCase) - { regex: /\b([A-Z][a-zA-Z0-9]*)\b/g, class: 'text-cyan-400' }, - ]; - - patterns.forEach(({ regex, class: className }) => { - highlighted = highlighted.replace(regex, `$&`); - }); - - return highlighted; - }; - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(code); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (error) { - console.error('Failed to copy to clipboard:', error); - } - }; - - const handleDownload = () => { - const blob = new Blob([code], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = fileName || 'code.txt'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - const lines = code.split('\n'); - const lineCount = lines.length; - - const CodeDisplay = ({ inModal = false }: { inModal?: boolean }) => ( -
    - {/* Header */} -
    -
    - - - {fileName || 'Untitled'} - - - {detectedLanguage} - - - {lineCount} lines - -
    -
    - - {!inModal && ( - - )} - -
    -
    - - {/* Code content */} -
    - - - {lines.map((line, idx) => ( - - - - - ))} - -
    - {idx + 1} - - -
    -
    -
    - ); - - return ( - <> - - - {/* Fullscreen Modal */} - {isFullscreen && ( -
    setIsFullscreen(false)} - > -
    e.stopPropagation()}> - - -
    -
    - )} - - ); -} \ No newline at end of file diff --git a/web/app/components/ConversationThread.tsx b/web/app/components/ConversationThread.tsx deleted file mode 100644 index 229f663..0000000 --- a/web/app/components/ConversationThread.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { MessageCircle, Clock, Sparkles, ChevronDown, ChevronRight, GitBranch, ArrowRight } from 'lucide-react'; -import { useState } from 'react'; -import { MessageFlow } from './MessageFlow' -import { formatLargeText } from '../utils/formatters'; - -interface ConversationThreadProps { - conversation: { - sessionId: string; - projectPath: string; - projectName: string; - messages: Array<{ - parentUuid: string | null; - isSidechain: boolean; - userType: string; - cwd: string; - sessionId: string; - version: string; - type: string; - message: any; - uuid: string; - timestamp: string; - }>; - startTime: string; - endTime: string; - messageCount: number; - }; -} - -interface ConversationMessage { - role: 'user' | 'assistant' | 'system'; - content: any; - timestamp: string; - turnNumber?: number; - isNewInTurn?: boolean; - isDuplicate?: boolean; -} - -export function ConversationThread({ conversation }: ConversationThreadProps) { - const [expandedSections, setExpandedSections] = useState>(new Set(['flow'])); - - const toggleSection = (section: string) => { - const newExpanded = new Set(expandedSections); - if (newExpanded.has(section)) { - newExpanded.delete(section); - } else { - newExpanded.add(section); - } - setExpandedSections(newExpanded); - }; - - // Extract all messages and analyze conversation flow from JSONL messages - const analyzeConversationFlow = () => { - const allMessages: ConversationMessage[] = []; - - // Check if messages exist - if (!conversation.messages || !Array.isArray(conversation.messages)) { - console.warn('No messages found in conversation:', conversation); - return allMessages; - } - - // Convert JSONL messages to conversation messages - conversation.messages.forEach((msg) => { - // Parse the message content - let parsedMessage: any; - try { - parsedMessage = typeof msg.message === 'string' ? JSON.parse(msg.message) : msg.message; - } catch (e) { - parsedMessage = msg.message; - } - - // Determine the role based on the type field - let role: 'user' | 'assistant' | 'system' = 'user'; - if (msg.type === 'assistant') { - role = 'assistant'; - } else if (msg.type === 'system') { - role = 'system'; - } - - // Extract content based on message structure - let content = null; - if (parsedMessage) { - if (parsedMessage.content) { - content = parsedMessage.content; - } else if (parsedMessage.text) { - content = parsedMessage.text; - } else if (Array.isArray(parsedMessage)) { - content = parsedMessage; - } else if (typeof parsedMessage === 'string') { - content = parsedMessage; - } else { - content = parsedMessage; - } - } - - if (content) { - allMessages.push({ - role, - content, - timestamp: msg.timestamp, - turnNumber: undefined, // Not available in JSONL format - isNewInTurn: true, - }); - } - }); - - return allMessages; - }; - - const messages = analyzeConversationFlow(); - - if (messages.length === 0) { - return ( -
    -
    - -
    -

    No messages found

    -

    This conversation appears to be empty

    -
    - ); - } - - return ( -
    - {/* Conversation Flow Header */} -
    -
    toggleSection('flow')} - > -
    -
    - -
    -
    -

    - Conversation Flow -
    - - - Conversation processed - - {messages.length} messages - -
    -

    -

    - {messages.length} messages • {conversation.messageCount} total -

    -
    -
    -
    - - {new Date(messages[messages.length - 1]?.timestamp).toLocaleTimeString()} - - {expandedSections.has('flow') ? ( - - ) : ( - - )} -
    -
    -
    - - {/* Conversation Messages */} - {expandedSections.has('flow') && ( -
    - {messages.map((message, index) => ( - - ))} - - {/* Conversation Summary */} -
    -
    -
    -
    - -
    -
    -
    Conversation Summary
    -
    - {messages.length} messages • {conversation.messageCount} total messages -
    -
    -
    -
    -
    - - Latest: {new Date().toLocaleTimeString()} -
    -
    -
    -
    -
    - )} -
    - ); -} \ No newline at end of file diff --git a/web/app/components/ImageContent.tsx b/web/app/components/ImageContent.tsx deleted file mode 100644 index 7be0a3d..0000000 --- a/web/app/components/ImageContent.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { useState } from 'react'; -import { Image as ImageIcon, Download, Maximize2, X } from 'lucide-react'; - -interface ImageContentProps { - content: { - source?: { - type: string; - media_type: string; - data: string; - }; - data?: string; - media_type?: string; - }; -} - -export function ImageContent({ content }: ImageContentProps) { - const [isFullscreen, setIsFullscreen] = useState(false); - const [imageError, setImageError] = useState(false); - - // Extract image data and media type - let imageData: string | undefined; - let mediaType: string | undefined; - - if (content.source) { - // Claude API format - imageData = content.source.data; - mediaType = content.source.media_type; - } else if (content.data) { - // Alternative format - imageData = content.data; - mediaType = content.media_type || 'image/png'; - } - - if (!imageData) { - return ( -
    -
    - - No image data available -
    -
    - ); - } - - // Ensure the data URI is properly formatted - const dataUri = imageData.startsWith('data:') - ? imageData - : `data:${mediaType || 'image/png'};base64,${imageData}`; - - const handleDownload = () => { - const link = document.createElement('a'); - link.href = dataUri; - link.download = `image-${Date.now()}.${mediaType?.split('/')[1] || 'png'}`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }; - - if (imageError) { - return ( -
    -
    - - Failed to load image -
    -
    - - Show raw data - -
    -            {JSON.stringify(content, null, 2)}
    -          
    -
    -
    - ); - } - - return ( - <> -
    -
    -
    - - - Image ({mediaType || 'unknown type'}) - -
    -
    - - -
    -
    -
    - Content image setIsFullscreen(true)} - onError={() => setImageError(true)} - /> -
    -
    - - {/* Fullscreen Modal */} - {isFullscreen && ( -
    setIsFullscreen(false)} - > -
    - - Content image (fullscreen) e.stopPropagation()} - /> -
    -
    - )} - - ); -} \ No newline at end of file diff --git a/web/app/components/MessageContent.tsx b/web/app/components/MessageContent.tsx deleted file mode 100644 index c45feb1..0000000 --- a/web/app/components/MessageContent.tsx +++ /dev/null @@ -1,400 +0,0 @@ -import { useState } from 'react'; -import { ChevronDown, ChevronRight, Wrench, Code, FileText, Database, AlertCircle } from 'lucide-react'; -import { ToolResult } from './ToolResult'; -import { ToolUse } from './ToolUse'; -import { ImageContent } from './ImageContent'; -import { formatLargeText } from '../utils/formatters'; - -interface ContentItem { - type: string; - text?: string; - content?: any; - name?: string; - id?: string; - input?: Record; - tool_call_id?: string; - is_error?: boolean; -} - -interface MessageContentProps { - content: ContentItem | ContentItem[] | string; -} - -export function MessageContent({ content }: MessageContentProps) { - // Handle string content - if (typeof content === 'string') { - // Check if content contains system reminders - if (content.includes('')) { - return ; - } - - return ( -
    - ); - } - - // Handle array of content items - if (Array.isArray(content)) { - return ( -
    - {content.map((item, index) => ( -
    - -
    - ))} -
    - ); - } - - // Handle single content item - if (content && typeof content === 'object') { - switch (content.type) { - case 'text': - // Check if this text contains tool definitions - if (content.text && content.text.includes('')) { - return ; - } - // Check if this text contains system reminders - if (content.text && content.text.includes('')) { - return ; - } - return ( -
    - ); - - case 'tool_use': - return ( - - ); - - case 'tool_result': - // Handle both content.text and content.content structures - const resultContent = content.text || content.content || content; - return ( - - ); - - case 'image': - return ; - - default: - return ( -
    -
    - - Unknown content type: {content.type} -
    -
    - - Show raw content - -
    -                {JSON.stringify(content, null, 2)}
    -              
    -
    -
    - ); - } - } - - // Fallback - return ( -
    -
    - - Unable to render content -
    -
    - - Show raw content - -
    -          {JSON.stringify(content, null, 2)}
    -        
    -
    -
    - ); -} - -// Component to handle tool definitions in system prompts -function ToolDefinitions({ text }: { text: string }) { - const [isExpanded, setIsExpanded] = useState(false); - - const functionsMatch = text.match(/([\s\S]*?)<\/functions>/); - if (!functionsMatch) { - return ( -
    - ); - } - - const functionsText = functionsMatch[1]; - const beforeFunctions = text.substring(0, functionsMatch.index!); - const afterFunctions = text.substring(functionsMatch.index! + functionsMatch[0].length); - - // Parse individual function definitions - const functionMatches = [...functionsText.matchAll(/([\s\S]*?)<\/function>/g)]; - - return ( -
    - {beforeFunctions && ( -
    - )} - -
    -
    -
    -
    - -
    -
    -
    - Available Tools - -
    -
    - {functionMatches.length} tools defined for this conversation -
    -
    -
    - -
    - - {isExpanded && ( -
    - {functionMatches.map((match, index) => ( - - ))} -
    - )} -
    - - {afterFunctions && ( -
    - )} -
    - ); -} - -// Component to render individual tool definition -function ToolDefinition({ functionText, index }: { functionText: string; index: number }) { - const [showDetails, setShowDetails] = useState(false); - - try { - const toolDef = JSON.parse(functionText); - const paramCount = toolDef.parameters?.properties ? Object.keys(toolDef.parameters.properties).length : 0; - const requiredParams = toolDef.parameters?.required || []; - - return ( -
    -
    -
    -
    - -
    -
    - {toolDef.name} -
    - - {paramCount} params - - {requiredParams.length > 0 && ( - - {requiredParams.length} required - - )} -
    -
    -
    - -
    - -
    - {toolDef.description || 'No description available'} -
    - - {showDetails && ( -
    - {toolDef.parameters?.properties && ( -
    -
    Parameters:
    -
    - {Object.entries(toolDef.parameters.properties).map(([name, param]: [string, any]) => ( -
    -
    - {name} - {requiredParams.includes(name) ? ( - - required - - ) : ( - - optional - - )} - {param.type || 'any'} -
    -
    - {param.description || 'No description'} -
    -
    - ))} -
    -
    - )} - -
    - - Show raw definition - -
    -                {JSON.stringify(toolDef, null, 2)}
    -              
    -
    -
    - )} -
    - ); - } catch (e) { - return ( -
    -
    - - Invalid Tool Definition #{index + 1} -
    -
    -          {functionText}
    -        
    -
    - ); - } -} - -// Component to handle system reminder content -function SystemReminderContent({ content }: { content: string }) { - const [showReminders, setShowReminders] = useState(false); - - // Split content into regular and system reminder parts - const parts: Array<{ type: 'text' | 'reminder'; content: string }> = []; - const reminderRegex = /([\s\S]*?)<\/system-reminder>/g; - let lastIndex = 0; - let match; - - while ((match = reminderRegex.exec(content)) !== null) { - // Add text before the reminder - if (match.index > lastIndex) { - const textPart = content.substring(lastIndex, match.index).trim(); - if (textPart) { - parts.push({ type: 'text', content: textPart }); - } - } - - // Add the reminder - parts.push({ type: 'reminder', content: match[1].trim() }); - lastIndex = match.index + match[0].length; - } - - // Add any remaining text - if (lastIndex < content.length) { - const textPart = content.substring(lastIndex).trim(); - if (textPart) { - parts.push({ type: 'text', content: textPart }); - } - } - - const reminderCount = parts.filter(p => p.type === 'reminder').length; - const hasNonReminderContent = parts.some(p => p.type === 'text'); - - return ( -
    - {/* Regular content */} - {parts.filter(p => p.type === 'text').map((part, index) => ( -
    - ))} - - {/* System reminder indicator/toggle */} - {reminderCount > 0 && ( -
    - - - {/* System reminder content */} - {showReminders && ( -
    - {parts.filter(p => p.type === 'reminder').map((part, index) => ( -
    -
    - -
    {part.content}
    -
    -
    - ))} -
    - )} -
    - )} - -
    - ); -} \ No newline at end of file diff --git a/web/app/components/MessageFlow.tsx b/web/app/components/MessageFlow.tsx deleted file mode 100644 index cb50167..0000000 --- a/web/app/components/MessageFlow.tsx +++ /dev/null @@ -1,282 +0,0 @@ -import { useState } from 'react'; -import { User, Bot, Settings, ChevronDown, ChevronRight, Clock, Sparkles, ArrowDown } from 'lucide-react'; -import { MessageContent } from './MessageContent'; -import { formatLargeText } from '../utils/formatters'; - -interface ConversationMessage { - role: 'user' | 'assistant' | 'system'; - content: any; - timestamp: string; - turnNumber?: number; - isNewInTurn?: boolean; - isDuplicate?: boolean; -} - -interface MessageFlowProps { - message: ConversationMessage; - index: number; - isLast: boolean; - totalMessages: number; -} - -export function MessageFlow({ message, index, isLast, totalMessages }: MessageFlowProps) { - const [isExpanded, setIsExpanded] = useState(false); - - const getRoleConfig = () => { - switch (message.role) { - case 'user': - return { - icon: , - bgColor: 'bg-blue-50', - borderColor: 'border-blue-200', - accentColor: 'border-l-blue-500', - textColor: 'text-blue-900', - titleColor: 'text-blue-700', - name: 'User' - }; - case 'assistant': - return { - icon: , - bgColor: 'bg-gray-50', - borderColor: 'border-gray-200', - accentColor: 'border-l-gray-500', - textColor: 'text-gray-900', - titleColor: 'text-gray-700', - name: 'Assistant' - }; - case 'system': - return { - icon: , - bgColor: 'bg-amber-50', - borderColor: 'border-amber-200', - accentColor: 'border-l-amber-500', - textColor: 'text-amber-900', - titleColor: 'text-amber-700', - name: 'System' - }; - default: - return { - icon: , - bgColor: 'bg-gray-50', - borderColor: 'border-gray-200', - accentColor: 'border-l-gray-500', - textColor: 'text-gray-900', - titleColor: 'text-gray-700', - name: 'Unknown' - }; - } - }; - - const roleConfig = getRoleConfig(); - - // Helper function to check if content is a system reminder - const isSystemReminder = (text: string) => { - return text.includes('') || text.includes(''); - }; - - // Helper function to extract non-system-reminder content for preview - const extractNonSystemContent = (content: string) => { - // Split by system-reminder tags and filter out the reminder parts - const parts = content.split(/[\s\S]*?<\/system-reminder>/g); - return parts.filter(part => part.trim()).join(' ').trim(); - }; - - // Determine if content should be expandable - const getContentPreview = () => { - if (typeof message.content === 'string') { - const nonSystemContent = extractNonSystemContent(message.content); - if (!nonSystemContent && isSystemReminder(message.content)) { - return "[System reminder]"; - } - return nonSystemContent.length > 300 ? nonSystemContent.substring(0, 300) + '...' : nonSystemContent; - } - - if (Array.isArray(message.content)) { - const allText = message.content - .filter(c => c.type === 'text' && c.text) - .map(c => { - const nonSystemContent = extractNonSystemContent(c.text); - return nonSystemContent; - }) - .filter(text => text) - .join('\\n'); - - if (!allText) { - const hasToolUse = message.content.some(c => c.type === 'tool_use'); - const hasSystemReminder = message.content.some(c => c.type === 'text' && c.text && isSystemReminder(c.text)); - if (hasToolUse) return "[Tool call]"; - if (hasSystemReminder) return "[System reminder]"; - return "[Context message]"; - } - - return allText.length > 300 ? allText.substring(0, 300) + '...' : allText; - } - - if (message.content?.type) { - return `[${message.content.type.replace('_', ' ')}]`; - } - - try { - const str = JSON.stringify(message.content, null, 2); - return str.length > 300 ? str.substring(0, 300) + '...' : str; - } catch { - return '[Complex content]'; - } - }; - - const shouldShowExpander = () => { - if (typeof message.content === 'string') { - // Show expander if content is long OR contains system reminders - return message.content.length > 300 || isSystemReminder(message.content); - } - - if (Array.isArray(message.content)) { - const allText = message.content - .filter(c => c.type === 'text' && c.text) - .map(c => c.text) - .join('\\n'); - return allText.length > 300 || message.content.length > 1; - } - - return true; - }; - - const formatTimestamp = (timestamp: string) => { - try { - const date = new Date(timestamp); - return date.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - hour12: true - }); - } catch { - return timestamp; - } - }; - - return ( -
    - {/* Connection line to next message */} - {!isLast && ( -
    - )} - - {/* Message container */} -
    - {/* New message indicator */} - {message.isNewInTurn && ( -
    - )} - -
    -
    - {/* Avatar */} -
    -
    - {roleConfig.icon} -
    -
    - - {/* Message content */} -
    - {/* Header */} -
    -
    - - {roleConfig.name} - - {message.isNewInTurn && ( - - NEW - - )} - - #{index + 1} - - {message.turnNumber && ( - - Turn {message.turnNumber} - - )} -
    -
    -
    - - {formatTimestamp(message.timestamp)} -
    -
    -
    - - {/* Content */} -
    - {shouldShowExpander() && !isExpanded ? ( -
    -
    - {typeof message.content === 'string' ? ( -
    - ) : ( -
    -
    - {Array.isArray(message.content) ? ( - `Message contains ${message.content.length} content blocks` - ) : ( - 'Complex content' - )} -
    - {Array.isArray(message.content) && ( -
    - {message.content.map(item => item.type).join(' → ')} -
    - )} -
    - {getContentPreview()} -
    -
    - )} -
    - -
    - ) : ( -
    - {shouldShowExpander() && isExpanded && ( -
    - -
    - )} - -
    - )} -
    -
    -
    -
    - - {/* Flow indicator */} - {!isLast && ( -
    - -
    - )} -
    -
    - ); -} \ No newline at end of file diff --git a/web/app/components/RequestDetailContent.tsx b/web/app/components/RequestDetailContent.tsx deleted file mode 100644 index 6b291c1..0000000 --- a/web/app/components/RequestDetailContent.tsx +++ /dev/null @@ -1,1024 +0,0 @@ -import { useState } from 'react'; -import { - ChevronDown, - Info, - Settings, - Cpu, - MessageCircle, - Brain, - User, - Bot, - Target, - Copy, - Check, - ArrowLeftRight, - Activity, - Clock, - Wifi, - Calendar, - List, - FileText, - Wrench -} from 'lucide-react'; -import { MessageContent } from './MessageContent'; -import { formatJSON } from '../utils/formatters'; -import { getChatCompletionsEndpoint, getProviderName } from '../utils/models'; - -interface Request { - id: number; - timestamp: string; - method: string; - endpoint: string; - headers: Record; - originalModel?: string; - routedModel?: string; - body?: { - model?: string; - messages?: Array<{ - role: string; - content: any; - }>; - system?: Array<{ - text: string; - type: string; - cache_control?: { type: string }; - }>; - tools?: Array<{ - name: string; - description: string; - input_schema?: { - type: string; - properties?: Record; - required?: string[]; - }; - }>; - max_tokens?: number; - temperature?: number; - stream?: boolean; - }; - response?: { - statusCode: number; - headers: Record; - body?: any; - bodyText?: string; - responseTime: number; - streamingChunks?: string[]; - isStreaming: boolean; - completedAt: string; - }; - promptGrade?: { - score: number; - criteria: Record; - feedback: string; - improvedPrompt: string; - gradingTimestamp: string; - }; -} - -interface RequestDetailContentProps { - request: Request; - onGrade: () => void; -} - -export default function RequestDetailContent({ request, onGrade }: RequestDetailContentProps) { - const [expandedSections, setExpandedSections] = useState>({ - overview: true, - // conversation: true - }); - const [copied, setCopied] = useState>({}); - - const toggleSection = (section: string) => { - setExpandedSections(prev => ({ - ...prev, - [section]: !prev[section] - })); - }; - - const handleCopy = async (content: string, key: string) => { - try { - await navigator.clipboard.writeText(content); - setCopied(prev => ({ ...prev, [key]: true })); - setTimeout(() => { - setCopied(prev => ({ ...prev, [key]: false })); - }, 2000); - } catch (error) { - console.error('Failed to copy to clipboard:', error); - } - }; - - const getMethodColor = (method: string) => { - const colors = { - 'GET': 'bg-green-50 text-green-700 border border-green-200', - 'POST': 'bg-blue-50 text-blue-700 border border-blue-200', - 'PUT': 'bg-yellow-50 text-yellow-700 border border-yellow-200', - 'DELETE': 'bg-red-50 text-red-700 border border-red-200' - }; - return colors[method as keyof typeof colors] || 'bg-gray-50 text-gray-700 border border-gray-200'; - }; - - const canGradeRequest = (request: Request) => { - return request.body && - request.body.messages && - request.body.messages.some(msg => msg.role === 'user') && - request.endpoint.includes('/messages'); - }; - - return ( -
    - {/* Request Overview */} -
    -
    -

    - - Request Overview -

    - {/* {!request.promptGrade && canGradeRequest(request) && ( - - )} */} -
    -
    -
    -
    - Method: - - {request.method} - -
    -
    - Endpoint: - - {getChatCompletionsEndpoint(request.routedModel, request.endpoint)} - -
    -
    -
    -
    - Timestamp: - {new Date(request.timestamp).toLocaleString()} -
    -
    - User Agent: - {request.headers['User-Agent']?.[0] || 'N/A'} -
    -
    -
    -
    - - {/* Headers */} -
    -
    toggleSection('headers')} - > -
    -

    - - Request Headers -

    - -
    -
    - {expandedSections.headers && ( -
    -
    -
    - Headers - -
    -
    -                {formatJSON(request.headers)}
    -              
    -
    -
    - )} -
    - - {request.body && ( - <> - {/* System Messages */} - {request.body.system && ( -
    -
    toggleSection('system')} - > -
    -

    - - System Instructions - - {request.body.system.length} items - -

    - -
    -
    - {expandedSections.system && ( -
    - {request.body.system.map((sys, index) => ( -
    -
    - System Message #{index + 1} - {sys.cache_control && ( - - Cache: {sys.cache_control.type} - - )} -
    -
    - -
    -
    - ))} -
    - )} -
    - )} - - {/* Tools */} - {request.body.tools && request.body.tools.length > 0 && ( -
    -
    toggleSection('tools')} - > -
    -

    - - Available Tools - - {request.body.tools.length} tools - -

    - -
    -
    - {expandedSections.tools && ( -
    - {request.body.tools.map((tool, index) => ( - - ))} -
    - )} -
    - )} - - {/* Conversation */} - {request.body.messages && ( -
    -
    toggleSection('conversation')} - > -
    -

    - - Conversation - - {request.body.messages.length} messages - -

    - -
    -
    - {expandedSections.conversation && ( -
    - {request.body.messages.map((message, index) => ( - - ))} -
    - )} -
    - )} - - {/* Model Configuration */} -
    -
    toggleSection('model')} - > -
    -

    - - Model Configuration -

    - -
    -
    - {expandedSections.model && ( -
    - {/* Model Routing Information */} - {request.routedModel && request.routedModel !== request.originalModel && ( -
    -
    -
    -
    - Requested Model - - {request.originalModel || request.body.model} - -
    -
    -
    - - Routed to -
    - - {request.routedModel} - - - {getProviderName(request.routedModel)} - -
    -
    -
    -
    Target Endpoint
    - - {getChatCompletionsEndpoint(request.routedModel)} - -
    -
    -
    - )} - - {/* Model Parameters */} -
    - {!request.routedModel || request.routedModel === request.originalModel ? ( -
    -
    Model
    -
    {request.originalModel || request.body.model || 'N/A'}
    -
    - ) : null} -
    -
    Max Tokens
    -
    - {request.body.max_tokens?.toLocaleString() || 'N/A'} -
    -
    -
    -
    Temperature
    -
    {request.body.temperature ?? 'N/A'}
    -
    -
    -
    Stream
    -
    - {request.body.stream ? '✅ Yes' : '❌ No'} -
    -
    -
    -
    - )} -
    - - )} - - {/* API Response */} - {request.response && ( - - )} - - {/* Prompt Grading Results */} - {request.promptGrade && ( - - )} -
    - ); -} - -// Message bubble component -function MessageBubble({ message, index }: { message: any; index: number }) { - const roleColors = { - 'user': 'bg-blue-50 border border-blue-200', - 'assistant': 'bg-gray-50 border border-gray-200', - 'system': 'bg-yellow-50 border border-yellow-200' - }; - - const roleIcons = { - 'user': User, - 'assistant': Bot, - 'system': Settings - }; - - const roleIconColors = { - 'user': 'text-blue-600', - 'assistant': 'text-gray-600', - 'system': 'text-yellow-600' - }; - - const Icon = roleIcons[message.role as keyof typeof roleIcons] || User; - - return ( -
    -
    -
    -
    - -
    - {message.role} - - #{index + 1} - -
    -
    -
    - -
    -
    - ); -} - -// Placeholder for prompt grading results - you can expand this -function PromptGradingResults({ promptGrade }: { promptGrade: any }) { - return ( -
    -

    Prompt Quality Analysis

    -
    -
    - Overall Score: - {promptGrade.score}/5 -
    -
    -

    {promptGrade.feedback}

    -
    -
    -
    - ); -} - -// Response Details Component -function ResponseDetails({ response }: { response: NonNullable }) { - const [expandedSections, setExpandedSections] = useState>({ - overview: true - }); - const [copied, setCopied] = useState>({}); - - const toggleSection = (section: string) => { - setExpandedSections(prev => ({ - ...prev, - [section]: !prev[section] - })); - }; - - const handleCopy = async (content: string, key: string) => { - try { - await navigator.clipboard.writeText(content); - setCopied(prev => ({ ...prev, [key]: true })); - setTimeout(() => { - setCopied(prev => ({ ...prev, [key]: false })); - }, 2000); - } catch (error) { - console.error('Failed to copy to clipboard:', error); - } - }; - - const getStatusColor = (statusCode: number) => { - if (statusCode >= 200 && statusCode < 300) { - return { bg: 'bg-green-50', border: 'border-green-200', text: 'text-green-700', icon: 'text-green-600' }; - } - if (statusCode >= 400 && statusCode < 500) { - return { bg: 'bg-yellow-50', border: 'border-yellow-200', text: 'text-yellow-700', icon: 'text-yellow-600' }; - } - if (statusCode >= 500) { - return { bg: 'bg-red-50', border: 'border-red-200', text: 'text-red-700', icon: 'text-red-600' }; - } - return { bg: 'bg-gray-50', border: 'border-gray-200', text: 'text-gray-700', icon: 'text-gray-600' }; - }; - - // Parse streaming chunks to extract the final assembled text - const parseStreamingResponse = (chunks: string[]) => { - let assembledText = ''; - let rawData = chunks.join(''); - - try { - // Split by lines and process each SSE event - const lines = rawData.split('\n').filter(line => line.trim()); - - for (const line of lines) { - // Look for data lines in SSE format - if (line.startsWith('data: ')) { - const jsonStr = line.substring(6).trim(); - - // Skip non-JSON lines (like "data: [DONE]") - if (!jsonStr.startsWith('{')) continue; - - try { - const eventData = JSON.parse(jsonStr); - - // Extract text from content_block_delta events - if (eventData.type === 'content_block_delta' && - eventData.delta && - eventData.delta.type === 'text_delta' && - typeof eventData.delta.text === 'string') { - assembledText += eventData.delta.text; - } - } catch (parseError) { - // Skip malformed JSON - continue; - } - } - } - - // If we successfully extracted text, return it - if (assembledText.trim().length > 0) { - return { - finalText: assembledText, - isFormatted: true, - rawData: rawData - }; - } - - // Fallback: try to find any text content in the raw data - const textMatches = rawData.match(/"text":"([^"]+)"/g); - if (textMatches) { - let fallbackText = ''; - for (const match of textMatches) { - const text = match.match(/"text":"([^"]+)"/)?.[1]; - if (text) { - // Unescape common JSON escape sequences - fallbackText += text.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\'); - } - } - if (fallbackText.trim()) { - return { - finalText: fallbackText, - isFormatted: true, - rawData: rawData - }; - } - } - - } catch (error) { - console.warn('Error parsing streaming response:', error); - } - - // Ultimate fallback to raw concatenation - return { - finalText: rawData, - isFormatted: false, - rawData: rawData - }; - }; - - const statusColors = getStatusColor(response.statusCode); - const completedAt = response.completedAt ? new Date(response.completedAt).toLocaleString() : 'Unknown'; - - return ( -
    -
    toggleSection('overview')} - > -
    -

    - - API Response - - {response.statusCode} - -

    - -
    -
    - - {expandedSections.overview && ( -
    - {/* Response Overview */} -
    -
    -
    - - Status -
    -
    {response.statusCode}
    -
    - {response.statusCode >= 200 && response.statusCode < 300 ? 'Success' : - response.statusCode >= 400 && response.statusCode < 500 ? 'Client Error' : - response.statusCode >= 500 ? 'Server Error' : 'Unknown'} -
    -
    - -
    -
    - - Response Time -
    -
    {response.responseTime}ms
    -
    - {response.responseTime < 1000 ? 'Fast' : response.responseTime < 3000 ? 'Normal' : 'Slow'} -
    -
    - -
    -
    - - Type -
    -
    - {response.isStreaming ? 'Stream' : 'Single'} -
    -
    - {response.isStreaming ? 'Streaming' : 'Complete'} -
    -
    - -
    -
    - - Completed -
    -
    {completedAt.split(' ')[1] || 'N/A'}
    -
    {completedAt.split(' ')[0] || ''}
    -
    -
    - - {/* Token Usage */} - {response.body?.usage && ( -
    -
    -
    - - Input Tokens -
    -
    - {response.body.usage.input_tokens?.toLocaleString() || '0'} -
    -
    Prompt
    -
    - -
    -
    - - Output Tokens -
    -
    - {response.body.usage.output_tokens?.toLocaleString() || '0'} -
    -
    Response
    -
    - -
    -
    - - Total Tokens -
    -
    - {((response.body.usage.input_tokens || 0) + (response.body.usage.output_tokens || 0)).toLocaleString()} -
    -
    Combined
    -
    - - {response.body.usage.cache_read_input_tokens && ( -
    -
    - - Cached Tokens -
    -
    - {response.body.usage.cache_read_input_tokens.toLocaleString()} -
    -
    From Cache
    -
    - )} -
    - )} - - {/* Response Headers */} - {response.headers && ( -
    -
    toggleSection('responseHeaders')} - > -
    -
    - - Response Headers - - {Object.keys(response.headers).length} - -
    - -
    -
    - {expandedSections.responseHeaders && ( -
    -
    -
    - Headers - -
    -
    -                      {formatJSON(response.headers)}
    -                    
    -
    -
    - )} -
    - )} - - {/* Response Body */} - {(response.body || response.bodyText) && ( -
    -
    toggleSection('responseBody')} - > -
    -
    - - Response Body - - {response.body ? 'JSON' : 'Text'} - -
    - -
    -
    - {expandedSections.responseBody && ( -
    -
    -
    - Response - -
    -
    -                      {response.body ? formatJSON(response.body) : response.bodyText}
    -                    
    -
    -
    - )} -
    - )} - - {/* Streaming Response */} - {response.isStreaming && response.streamingChunks && response.streamingChunks.length > 0 && (() => { - const parsed = parseStreamingResponse(response.streamingChunks); - return ( -
    -
    toggleSection('streamingResponse')} - > -
    -
    - - Streaming Response - - {response.streamingChunks.length} chunks - - {parsed.isFormatted && ( - - Parsed - - )} -
    - -
    -
    - {expandedSections.streamingResponse && ( -
    - {/* Clean Parsed Response */} - {parsed.isFormatted && ( -
    -
    -
    - - Final Response (Clean) -
    - -
    -
    -
    -                            {parsed.finalText}
    -                          
    -
    -
    - Extracted clean text from streaming chunks -
    -
    - )} - - {/* Raw Data (Collapsible) */} -
    -
    toggleSection('rawStreamingData')} - > - - - Raw Streaming Data - - -
    - {expandedSections.rawStreamingData && ( -
    -
    - SSE Events & Metadata - -
    -
    -                            {parsed.rawData}
    -                          
    -
    - )} -
    - -
    - {parsed.isFormatted - ? `Successfully parsed ${response.streamingChunks.length} streaming chunks` - : `Raw display of ${response.streamingChunks.length} streaming chunks (parsing failed)` - } -
    -
    - )} -
    - ); - })()} -
    - )} -
    - ); -} - -// Tool Card Component -function ToolCard({ tool, index }: { tool: any; index: number }) { - const [expanded, setExpanded] = useState(false); - const [copiedSchema, setCopiedSchema] = useState(false); - - const handleCopySchema = async () => { - try { - await navigator.clipboard.writeText(formatJSON(tool.input_schema)); - setCopiedSchema(true); - setTimeout(() => setCopiedSchema(false), 2000); - } catch (error) { - console.error('Failed to copy schema:', error); - } - }; - - // Parse description to identify code blocks and format them - const formatDescription = (description: string) => { - // Split by code blocks (text between backticks) - const parts = description.split(/(`[^`]+`)/g); - - return parts.map((part, i) => { - if (part.startsWith('`') && part.endsWith('`')) { - // Code inline - const code = part.slice(1, -1); - return ( - - {code} - - ); - } - - // Return non-code parts as plain text - return {part}; - }); - }; - - const isLongDescription = tool.description.length > 300; - const displayDescription = expanded ? tool.description : tool.description.slice(0, 300); - - return ( -
    -
    -
    -
    -
    - -
    -
    -
    {tool.name}
    - Tool #{index + 1} -
    -
    -
    - -
    -
    -
    - {formatDescription(displayDescription)} - {isLongDescription && !expanded && '...'} -
    - {isLongDescription && ( - - )} -
    -
    - - {tool.input_schema && ( -
    -
    -
    - - - Input Schema - - -
    -
    -
    -                  {formatJSON(tool.input_schema)}
    -                
    -
    -
    -
    - )} -
    -
    - ); -} \ No newline at end of file diff --git a/web/app/components/TodoList.tsx b/web/app/components/TodoList.tsx deleted file mode 100644 index 29b3167..0000000 --- a/web/app/components/TodoList.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { CheckSquare, Square, Clock, AlertCircle, ListTodo } from 'lucide-react'; - -interface Todo { - task?: string; - description?: string; - content?: string; - title?: string; - text?: string; - priority: 'high' | 'medium' | 'low'; - status: 'pending' | 'in_progress' | 'completed'; - [key: string]: any; // Allow other properties -} - -interface TodoListProps { - todos: Todo[]; -} - -export function TodoList({ todos }: TodoListProps) { - if (!todos || todos.length === 0) { - return ( -
    - -

    No tasks in the todo list

    -
    - ); - } - - const getPriorityColor = (priority: string) => { - switch (priority) { - case 'high': - return 'text-red-600 bg-red-50 border-red-200'; - case 'medium': - return 'text-yellow-600 bg-yellow-50 border-yellow-200'; - case 'low': - return 'text-green-600 bg-green-50 border-green-200'; - default: - return 'text-gray-600 bg-gray-50 border-gray-200'; - } - }; - - const getStatusIcon = (status: string) => { - switch (status) { - case 'completed': - return ; - case 'in_progress': - return ; - case 'pending': - return ; - default: - return ; - } - }; - - const getStatusColor = (status: string) => { - switch (status) { - case 'completed': - return 'bg-green-50 border-green-200'; - case 'in_progress': - return 'bg-blue-50 border-blue-200'; - case 'pending': - return 'bg-gray-50 border-gray-200'; - default: - return 'bg-gray-50 border-gray-200'; - } - }; - - // Group todos by status - const groupedTodos = { - in_progress: todos.filter(t => t.status === 'in_progress'), - pending: todos.filter(t => t.status === 'pending'), - completed: todos.filter(t => t.status === 'completed') - }; - - return ( -
    - {/* Summary stats */} -
    -
    - - Todo List -
    -
    - {groupedTodos.in_progress.length > 0 && ( - - {groupedTodos.in_progress.length} in progress - - )} - {groupedTodos.pending.length > 0 && ( - - {groupedTodos.pending.length} pending - - )} - {groupedTodos.completed.length > 0 && ( - - {groupedTodos.completed.length} completed - - )} -
    -
    - - {/* Todo items */} -
    - {/* In Progress items first */} - {groupedTodos.in_progress.map((todo, index) => ( - - ))} - - {/* Pending items */} - {groupedTodos.pending.map((todo, index) => ( - - ))} - - {/* Completed items last */} - {groupedTodos.completed.map((todo, index) => ( - - ))} -
    -
    - ); -} - -function TodoItem({ todo }: { todo: Todo }) { - // Get the task text from various possible property names - const getTaskText = (todo: Todo): string => { - return todo.task || todo.description || todo.content || todo.title || todo.text || - Object.entries(todo).find(([key, value]) => - typeof value === 'string' && - !['priority', 'status'].includes(key) - )?.[1] || - 'No task description'; - }; - - const taskText = getTaskText(todo); - const getPriorityColor = (priority: string) => { - switch (priority) { - case 'high': - return 'text-red-600 bg-red-50 border-red-200'; - case 'medium': - return 'text-yellow-600 bg-yellow-50 border-yellow-200'; - case 'low': - return 'text-green-600 bg-green-50 border-green-200'; - default: - return 'text-gray-600 bg-gray-50 border-gray-200'; - } - }; - - const getStatusIcon = (status: string) => { - switch (status) { - case 'completed': - return ; - case 'in_progress': - return ; - case 'pending': - return ; - default: - return ; - } - }; - - const getStatusColor = (status: string) => { - switch (status) { - case 'completed': - return 'bg-green-50 border-green-200'; - case 'in_progress': - return 'bg-blue-50 border-blue-200'; - case 'pending': - return 'bg-gray-50 border-gray-200'; - default: - return 'bg-gray-50 border-gray-200'; - } - }; - - return ( -
    -
    - {getStatusIcon(todo.status)} -
    -
    -

    - {taskText} -

    -
    -
    - - {todo.priority} - -
    -
    - ); -} \ No newline at end of file diff --git a/web/app/components/ToolResult.tsx b/web/app/components/ToolResult.tsx deleted file mode 100644 index b51901c..0000000 --- a/web/app/components/ToolResult.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { useState } from 'react'; -import { ChevronDown, ChevronRight, CheckCircle, AlertCircle, FileText, Database, Clock } from 'lucide-react'; -import { formatValue, formatJSON, isComplexObject, truncateText } from '../utils/formatters'; -import { CodeViewer } from './CodeViewer'; - -interface ToolResultProps { - content: any; - toolId?: string; - isError?: boolean; -} - -export function ToolResult({ content, toolId, isError = false }: ToolResultProps) { - const [isExpanded, setIsExpanded] = useState(false); - - // Detect if this is likely code content from a Read tool - const isCodeContent = (content: string): boolean => { - if (typeof content !== 'string') return false; - - // Check for line numbers pattern (e.g., " 1→" from cat -n output) - const hasLineNumbers = /^\s*\d+→/m.test(content); - - // Check for common code patterns - const hasCodePatterns = ( - content.includes('function') || - content.includes('const ') || - content.includes('let ') || - content.includes('var ') || - content.includes('import ') || - content.includes('export ') || - content.includes('class ') || - content.includes('interface ') || - content.includes('type ') || - content.includes('def ') || - content.includes('if (') || - content.includes('for (') || - content.includes('while (') || - content.includes('{') && content.includes('}') - ); - - // Check for file extension indicators in the content - const hasFileExtension = /\.(js|jsx|ts|tsx|py|rb|go|rs|java|cpp|c|h|cs|php|swift|kt|scala|r|sh|bash|sql|html|css|json|yaml|yml|toml|md|xml)$/m.test(content); - - return hasLineNumbers || (hasCodePatterns && content.length > 100); - }; - - // Extract code from cat -n format if present - const extractCodeFromCatN = (content: string): { code: string; fileName?: string } => { - if (typeof content !== 'string') return { code: content }; - - // Check if this is cat -n output - if (!/^\s*\d+→/m.test(content)) { - return { code: content }; - } - - // Extract the code by removing line numbers - const lines = content.split('\n'); - const codeLines = lines.map(line => { - // Match line number pattern and extract the code part - const match = line.match(/^\s*\d+→(.*)$/); - return match ? match[1] : line; - }); - - return { code: codeLines.join('\n') }; - }; - - // Handle different content structures - const getDisplayContent = () => { - // If content is a string, return it directly - if (typeof content === 'string') { - return content; - } - - // If content has a 'text' property, use that - if (content && typeof content === 'object' && 'text' in content) { - return content.text; - } - - // If content has a 'content' property, use that - if (content && typeof content === 'object' && 'content' in content) { - return content.content; - } - - // If it's an array, join with newlines - if (Array.isArray(content)) { - return content.map(item => formatValue(item)).join('\n'); - } - - // For complex objects, show JSON - if (isComplexObject(content)) { - return formatJSON(content); - } - - // Fallback to string conversion - return formatValue(content); - }; - - const displayContent = getDisplayContent(); - const isLargeContent = displayContent.length > 500; - const shouldTruncate = isLargeContent && !isExpanded; - const truncatedContent = shouldTruncate ? truncateText(displayContent, 500) : displayContent; - - // Determine if content should be rendered as JSON - const isJSONContent = isComplexObject(content) || (typeof content === 'string' && content.startsWith('{')); - - // Check if this is code content - const isCode = isCodeContent(displayContent); - const { code: extractedCode } = isCode ? extractCodeFromCatN(displayContent) : { code: displayContent }; - - const getResultConfig = () => { - if (isError) { - return { - bgColor: 'bg-gradient-to-r from-red-50 to-pink-50', - borderColor: 'border-red-200', - accentColor: 'border-l-red-500', - iconBg: 'bg-red-100', - iconColor: 'text-red-600', - titleColor: 'text-red-900', - icon: , - title: 'Tool Error' - }; - } - - return { - bgColor: 'bg-gradient-to-r from-emerald-50 to-green-50', - borderColor: 'border-emerald-200', - accentColor: 'border-l-emerald-500', - iconBg: 'bg-emerald-100', - iconColor: 'text-emerald-600', - titleColor: 'text-emerald-900', - icon: , - title: 'Tool Result' - }; - }; - - const config = getResultConfig(); - - return ( -
    - {/* Header */} -
    -
    -
    -
    - {config.icon} -
    -
    -
    -
    - - {config.title} - - -
    - {toolId && ( -
    - - - {toolId} - -
    - )} -
    -
    - - {isLargeContent && ( - - )} -
    - - {/* Content */} -
    -
    - {/* Content type indicator */} -
    -
    - - Result received -
    -
    - - {isCode ? 'Code' : isJSONContent ? 'JSON' : 'Text'} - - {!isCode && ( - - {displayContent.length} chars - - )} -
    -
    - - {/* Main content */} - {isCode ? ( - - ) : isJSONContent ? ( -
    -              {truncatedContent}
    -            
    - ) : ( -
    ') - }} - /> - )} - - {/* Expand/collapse controls */} - {shouldTruncate && !isCode && ( -
    - -
    - )} -
    -
    - - {/* Metadata */} - {content && typeof content === 'object' && Object.keys(content).length > 1 && ( -
    -
    - - - Show raw data structure - -
    -
    -                {formatJSON(content)}
    -              
    -
    -
    -
    - )} - - {/* Result indicator */} -
    -
    -
    - {isError ? 'Execution failed' : 'Execution completed'} -
    -
    -
    - ); -} \ No newline at end of file diff --git a/web/app/components/ToolUse.tsx b/web/app/components/ToolUse.tsx deleted file mode 100644 index 46084a4..0000000 --- a/web/app/components/ToolUse.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { useState } from 'react'; -import { Wrench, ChevronDown, ChevronRight, Copy, Check, Terminal, Zap } from 'lucide-react'; -import { formatValue, formatJSON, isComplexObject } from '../utils/formatters'; -import { CodeDiff } from './CodeDiff'; -import { TodoList } from './TodoList'; - -interface ToolUseProps { - name: string; - id: string; - input?: Record; - text?: string; -} - -export function ToolUse({ name, id, input = {}, text }: ToolUseProps) { - const [isExpanded, setIsExpanded] = useState(false); - const [copied, setCopied] = useState(false); - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(formatJSON({ name, id, input })); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch (error) { - console.error('Failed to copy to clipboard:', error); - } - }; - - const renderParameterValue = (value: any) => { - if (typeof value === 'string') { - if (value.length > 200 || value.includes('\n')) { - return ( -
    - - {isExpanded && ( -
    -                {value}
    -              
    - )} -
    - ); - } - return {value}; - } - - if (isComplexObject(value)) { - return ( -
    - - Show object ({Object.keys(value).length} properties) - -
    -            {formatJSON(value)}
    -          
    -
    - ); - } - - return {formatValue(value)}; - }; - - return ( -
    - {/* Header */} -
    -
    -
    - -
    -
    -
    - Tool Execution - -
    -
    - - - {name} - -
    -
    -
    -
    - - {id} - - -
    -
    - - {/* Special handling for Edit tool - show code diff */} - {name === 'Edit' && input.old_string && input.new_string && ( -
    -
    Code Changes
    - -
    - )} - - {/* Special handling for Read tool - show code with syntax highlighting */} - {name === 'Read' && input.file_path && ( -
    -
    File Contents
    - {/* Note: The actual file content will be in the tool result, not the input */} -
    - Reading: {input.file_path} -
    -
    - )} - - {/* Special handling for TodoWrite tool - show todo list */} - {name === 'TodoWrite' && input.todos && Array.isArray(input.todos) && ( -
    -
    Task Management
    - -
    - )} - - {/* Parameters */} - {Object.keys(input).length > 0 && ( -
    -
    -
    - Parameters - - {Object.keys(input).length} - -
    - {Object.keys(input).length > 2 && ( - - )} -
    - - {/* Don't show raw parameters for Edit and TodoWrite tools since we have custom views */} - {name !== 'Edit' && name !== 'TodoWrite' && ( -
    -
    2 ? 'max-h-32 overflow-hidden' : ''}`}> - {Object.entries(input).map(([key, value]) => ( -
    - - {key}: - -
    - {renderParameterValue(value)} -
    -
    - ))} -
    - - {!isExpanded && Object.keys(input).length > 2 && ( -
    - -
    - )} -
    - )} -
    - )} - - {/* Additional text */} - {text && ( -
    -
    Additional Information:
    -
    {text}
    -
    - )} - - {/* Tool execution indicator */} -
    -
    -
    - Tool execution initiated -
    -
    -
    - ); -} \ No newline at end of file diff --git a/web/app/entry.client.tsx b/web/app/entry.client.tsx deleted file mode 100644 index 94d5dc0..0000000 --- a/web/app/entry.client.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * By default, Remix will handle hydrating your app on the client for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.client - */ - -import { RemixBrowser } from "@remix-run/react"; -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; - -startTransition(() => { - hydrateRoot( - document, - - - - ); -}); diff --git a/web/app/entry.server.tsx b/web/app/entry.server.tsx deleted file mode 100644 index 45db322..0000000 --- a/web/app/entry.server.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/** - * By default, Remix will handle generating the HTTP Response for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.server - */ - -import { PassThrough } from "node:stream"; - -import type { AppLoadContext, EntryContext } from "@remix-run/node"; -import { createReadableStreamFromReadable } from "@remix-run/node"; -import { RemixServer } from "@remix-run/react"; -import { isbot } from "isbot"; -import { renderToPipeableStream } from "react-dom/server"; - -const ABORT_DELAY = 5_000; - -export default function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, - // This is ignored so we can keep it in the template for visibility. Feel - // free to delete this parameter in your app if you're not using it! - // eslint-disable-next-line @typescript-eslint/no-unused-vars - loadContext: AppLoadContext -) { - return isbot(request.headers.get("user-agent") || "") - ? handleBotRequest( - request, - responseStatusCode, - responseHeaders, - remixContext - ) - : handleBrowserRequest( - request, - responseStatusCode, - responseHeaders, - remixContext - ); -} - -function handleBotRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onAllReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }) - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - } - ); - - setTimeout(abort, ABORT_DELAY); - }); -} - -function handleBrowserRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onShellReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }) - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - } - ); - - setTimeout(abort, ABORT_DELAY); - }); -} diff --git a/web/app/root.tsx b/web/app/root.tsx deleted file mode 100644 index 61c8b98..0000000 --- a/web/app/root.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { - Links, - Meta, - Outlet, - Scripts, - ScrollRestoration, -} from "@remix-run/react"; -import type { LinksFunction } from "@remix-run/node"; - -import "./tailwind.css"; - -export const links: LinksFunction = () => [ - { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - }, -]; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ); -} - -export default function App() { - return ; -} diff --git a/web/app/routes/_index.tsx b/web/app/routes/_index.tsx deleted file mode 100644 index 9908607..0000000 --- a/web/app/routes/_index.tsx +++ /dev/null @@ -1,914 +0,0 @@ -import type { MetaFunction } from "@remix-run/node"; -import { useState, useEffect, useTransition } from "react"; -import { - Activity, - RefreshCw, - Trash2, - List, - FileText, - X, - ChevronRight, - ChevronDown, - Inbox, - Wrench, - Bot, - User, - Settings, - Zap, - Users, - Target, - Cpu, - MessageCircle, - Brain, - CheckCircle, - ClipboardCheck, - BarChart3, - MessageSquare, - Sparkles, - Copy, - Check, - Lightbulb, - Loader2, - ArrowLeftRight -} from "lucide-react"; - -import RequestDetailContent from "../components/RequestDetailContent"; -import { ConversationThread } from "../components/ConversationThread"; -import { getChatCompletionsEndpoint } from "../utils/models"; - -export const meta: MetaFunction = () => { - return [ - { title: "Claude Code Monitor" }, - { name: "description", content: "Claude Code Monitor - Real-time API request visualization" }, - ]; -}; - -interface Request { - id: number; - conversationId?: string; - turnNumber?: number; - isRoot?: boolean; - timestamp: string; - method: string; - endpoint: string; - headers: Record; - originalModel?: string; - routedModel?: string; - body?: { - model?: string; - messages?: Array<{ - role: string; - content: any; - }>; - system?: Array<{ - text: string; - type: string; - cache_control?: { type: string }; - }>; - tools?: Array<{ - name: string; - description: string; - input_schema?: { - type: string; - properties?: Record; - required?: string[]; - }; - }>; - max_tokens?: number; - temperature?: number; - stream?: boolean; - }; - response?: { - statusCode: number; - headers: Record; - body?: { - usage?: { - input_tokens?: number; - output_tokens?: number; - cache_creation_input_tokens?: number; - cache_read_input_tokens?: number; - service_tier?: string; - }; - [key: string]: any; - }; - bodyText?: string; - responseTime: number; - streamingChunks?: string[]; - isStreaming: boolean; - completedAt: string; - }; - promptGrade?: { - score: number; - criteria: Record; - feedback: string; - improvedPrompt: string; - gradingTimestamp: string; - }; -} - -interface ConversationSummary { - id: string; - requestCount: number; - startTime: string; - lastActivity: string; - duration: number; - firstMessage: string; - lastMessage: string; - projectName: string; -} - -interface Conversation { - sessionId: string; - projectPath: string; - projectName: string; - messages: Array<{ - parentUuid: string | null; - isSidechain: boolean; - userType: string; - cwd: string; - sessionId: string; - version: string; - type: 'user' | 'assistant'; - message: any; - uuid: string; - timestamp: string; - }>; - startTime: string; - endTime: string; - messageCount: number; -} - -export default function Index() { - const [requests, setRequests] = useState([]); - const [conversations, setConversations] = useState([]); - const [selectedRequest, setSelectedRequest] = useState(null); - const [selectedConversation, setSelectedConversation] = useState(null); - const [filter, setFilter] = useState("all"); - const [viewMode, setViewMode] = useState<"requests" | "conversations">("requests"); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isConversationModalOpen, setIsConversationModalOpen] = useState(false); - const [modelFilter, setModelFilter] = useState("all"); - const [isFetching, setIsFetching] = useState(false); - const [isPending, startTransition] = useTransition(); - const [requestsCurrentPage, setRequestsCurrentPage] = useState(1); - const [hasMoreRequests, setHasMoreRequests] = useState(true); - const [conversationsCurrentPage, setConversationsCurrentPage] = useState(1); - const [hasMoreConversations, setHasMoreConversations] = useState(true); - const itemsPerPage = 50; - - const loadRequests = async (filter?: string, loadMore = false) => { - setIsFetching(true); - const pageToFetch = loadMore ? requestsCurrentPage + 1 : 1; - try { - const currentModelFilter = filter || modelFilter; - const url = new URL('/api/requests', window.location.origin); - url.searchParams.append("page", pageToFetch.toString()); - url.searchParams.append("limit", itemsPerPage.toString()); - if (currentModelFilter !== "all") { - url.searchParams.append("model", currentModelFilter); - } - - const response = await fetch(url.toString()); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - const requests = data.requests || []; - const mappedRequests = requests.map((req: any, index: number) => ({ - ...req, - id: req.requestId ? `${req.requestId}_${index}` : `request_${index}` - })); - - startTransition(() => { - if (loadMore) { - setRequests(prev => [...prev, ...mappedRequests]); - } else { - setRequests(mappedRequests); - } - setRequestsCurrentPage(pageToFetch); - setHasMoreRequests(mappedRequests.length === itemsPerPage); - }); - } catch (error) { - console.error('Failed to load requests:', error); - startTransition(() => { - setRequests([]); - }); - } finally { - setIsFetching(false); - } - }; - - const loadConversations = async (modelFilter: string = "all", loadMore = false) => { - setIsFetching(true); - const pageToFetch = loadMore ? conversationsCurrentPage + 1 : 1; - try { - const url = new URL('/api/conversations', window.location.origin); - url.searchParams.append("page", pageToFetch.toString()); - url.searchParams.append("limit", itemsPerPage.toString()); - if (modelFilter !== "all") { - url.searchParams.append("model", modelFilter); - } - - const response = await fetch(url.toString()); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - startTransition(() => { - if (loadMore) { - setConversations(prev => [...prev, ...data.conversations]); - } else { - setConversations(data.conversations); - } - setConversationsCurrentPage(pageToFetch); - setHasMoreConversations(data.conversations.length === itemsPerPage); - }); - } catch (error) { - console.error('Failed to load conversations:', error); - startTransition(() => { - setConversations([]); - }); - } finally { - setIsFetching(false); - } - }; - - const loadConversationDetails = async (conversationId: string, projectName: string) => { - try { - const response = await fetch(`/api/conversations/${conversationId}?project=${encodeURIComponent(projectName)}`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const conversation = await response.json(); - setSelectedConversation(conversation); - setIsConversationModalOpen(true); - } catch (error) { - console.error('Failed to load conversation details:', error); - } - }; - - const clearRequests = async () => { - try { - const response = await fetch('/api/requests', { - method: 'DELETE' - }); - - if (response.ok) { - setRequests([]); - setConversations([]); - setRequestsCurrentPage(1); - setHasMoreRequests(true); - setConversationsCurrentPage(1); - setHasMoreConversations(true); - } - } catch (error) { - console.error('Failed to clear requests:', error); - setRequests([]); - } - }; - - const filterRequests = (filter: string) => { - if (filter === 'all') return requests; - - return requests.filter(req => { - switch (filter) { - case 'messages': - return req.endpoint.includes('/messages'); - case 'completions': - return req.endpoint.includes('/completions'); - case 'models': - return req.endpoint.includes('/models'); - default: - return true; - } - }); - }; - - const getMethodColor = (method: string) => { - const colors = { - 'GET': 'bg-green-50 text-green-700 border border-green-200', - 'POST': 'bg-blue-50 text-blue-700 border border-blue-200', - 'PUT': 'bg-yellow-50 text-yellow-700 border border-yellow-200', - 'DELETE': 'bg-red-50 text-red-700 border border-red-200' - }; - return colors[method as keyof typeof colors] || 'bg-gray-50 text-gray-700 border border-gray-200'; - }; - - const getRequestSummary = (request: Request) => { - const parts = []; - - // Add token usage if available - if (request.response?.body?.usage) { - const usage = request.response.body.usage; - const inputTokens = usage.input_tokens || 0; - const outputTokens = usage.output_tokens || 0; - const totalTokens = inputTokens + outputTokens; - - if (totalTokens > 0) { - parts.push(`🪙 ${totalTokens.toLocaleString()} tokens`); - - if (usage.cache_read_input_tokens) { - parts.push(`💾 ${usage.cache_read_input_tokens.toLocaleString()} cached`); - } - } - } - - // Add response time if available - if (request.response?.responseTime) { - const seconds = (request.response.responseTime / 1000).toFixed(1); - parts.push(`⏱️ ${seconds}s`); - } - - // Add model if available (use routed model if different from original) - const model = request.routedModel || request.body?.model; - if (model) { - const modelShort = model.includes('opus') ? 'Opus' : - model.includes('sonnet') ? 'Sonnet' : - model.includes('haiku') ? 'Haiku' : - model.includes('gpt-4o') ? 'gpt-4o' : - model.includes('o3') ? 'o3' : - model.includes('o3-mini') ? 'o3-mini' : 'Model'; - parts.push(`🤖 ${modelShort}`); - - // Show routing info if model was routed - if (request.routedModel && request.originalModel && request.routedModel !== request.originalModel) { - parts.push(`→ routed`); - } - } - - return parts.length > 0 ? parts.join(' • ') : '📡 API request'; - }; - - const showRequestDetails = (requestId: number) => { - const request = requests.find(r => r.id === requestId); - if (request) { - setSelectedRequest(request); - setIsModalOpen(true); - } - }; - - const closeModal = () => { - setIsModalOpen(false); - setSelectedRequest(null); - }; - - const getToolStats = () => { - let toolDefinitions = 0; - let toolCalls = 0; - - requests.forEach(req => { - if (req.body) { - // Count tool definitions in system prompts - if (req.body.system) { - req.body.system.forEach(sys => { - if (sys.text && sys.text.includes('')) { - const functionMatches = [...sys.text.matchAll(/([\s\S]*?)<\/function>/g)]; - toolDefinitions += functionMatches.length; - } - }); - } - - // Count actual tool calls in messages - if (req.body.messages) { - req.body.messages.forEach(msg => { - if (msg.content && Array.isArray(msg.content)) { - msg.content.forEach((contentPart: any) => { - if (contentPart.type === 'tool_use') { - toolCalls++; - } - if (contentPart.type === 'text' && contentPart.text && contentPart.text.includes('')) { - const functionMatches = [...contentPart.text.matchAll(/([\s\S]*?)<\/function>/g)]; - toolDefinitions += functionMatches.length; - } - }); - } - }); - } - } - }); - - return `${toolCalls} calls / ${toolDefinitions} tools`; - }; - - const getPromptGradeStats = () => { - let totalGrades = 0; - let gradeCount = 0; - - requests.forEach(req => { - if (req.promptGrade && req.promptGrade.score) { - totalGrades += req.promptGrade.score; - gradeCount++; - } - }); - - if (gradeCount > 0) { - const avgGrade = (totalGrades / gradeCount).toFixed(1); - return `${avgGrade}/5`; - } - return '-/5'; - }; - - const formatDuration = (milliseconds: number) => { - if (milliseconds < 60000) { - return `${Math.round(milliseconds / 1000)}s`; - } else if (milliseconds < 3600000) { - return `${Math.round(milliseconds / 60000)}m`; - } else { - return `${Math.round(milliseconds / 3600000)}h`; - } - }; - - const formatConversationSummary = (conversation: ConversationSummary) => { - const duration = formatDuration(conversation.duration); - return `${conversation.requestCount} requests • ${duration} duration`; - }; - - const canGradeRequest = (request: Request) => { - return request.body && - request.body.messages && - request.body.messages.some(msg => msg.role === 'user') && - request.endpoint.includes('/messages'); - }; - - const gradeRequest = async (requestId: number) => { - const request = requests.find(r => r.id === requestId); - if (!request || !canGradeRequest(request)) return; - - try { - const response = await fetch('/api/grade-prompt', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - messages: request.body!.messages, - systemMessages: request.body!.system || [], - requestId: request.timestamp - }) - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const promptGrade = await response.json(); - - // Update the request with the new grading - const updatedRequests = requests.map(r => - r.id === requestId ? { ...r, promptGrade } : r - ); - setRequests(updatedRequests); - - } catch (error) { - console.error('Failed to grade prompt:', error); - } - }; - - const handleModelFilterChange = (newFilter: string) => { - setModelFilter(newFilter); - if (viewMode === 'requests') { - loadRequests(newFilter); - } else { - loadConversations(newFilter); - } - }; - - useEffect(() => { - if (viewMode === 'requests') { - loadRequests(modelFilter); - } else { - loadConversations(modelFilter); - } - }, [viewMode, modelFilter]); - - // Handle escape key to close modals - useEffect(() => { - const handleEscapeKey = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - if (isModalOpen) { - closeModal(); - } else if (isConversationModalOpen) { - setIsConversationModalOpen(false); - setSelectedConversation(null); - } - } - }; - - window.addEventListener('keydown', handleEscapeKey); - - return () => { - window.removeEventListener('keydown', handleEscapeKey); - }; - }, [isModalOpen, isConversationModalOpen]); - - const filteredRequests = filterRequests(filter); - - return ( -
    - {/* Header */} -
    -
    -
    -
    -

    Claude Code Monitor

    -
    -
    - - -
    -
    -
    -
    - - {/* View mode toggle */} -
    -
    - - -
    -
    - - {/* Filter buttons - only show for requests view */} - {viewMode === "requests" && ( -
    -
    - - - - -
    -
    - )} - - {/* Main Content */} -
    - {/* Stats Grid */} -
    -
    -
    -
    -

    - {viewMode === "requests" ? "Total Requests" : "Total Conversations"} -

    -

    - {viewMode === "requests" ? requests.length : conversations.length} -

    -
    -
    -
    -
    - - {/* Main Content */} - {viewMode === "requests" ? ( - /* Request History */ -
    -
    -
    -

    Request History

    -
    -
    -
    - {(isFetching && requestsCurrentPage === 1) || isPending ? ( -
    - -

    Loading requests...

    -
    - ) : filteredRequests.length === 0 ? ( -
    -

    No requests found

    -

    Make sure you have set ANTHROPIC_BASE_URL to point at the proxy

    -
    - ) : ( - <> - {filteredRequests.map(request => ( -
    showRequestDetails(request.id)}> -
    -
    - {/* Model and Status */} -
    -

    - {request.routedModel || request.body?.model ? ( - // Use routedModel if available, otherwise fall back to body.model - (() => { - const model = request.routedModel || request.body?.model || ''; - if (model.includes('opus')) return Opus; - if (model.includes('sonnet')) return Sonnet; - if (model.includes('haiku')) return Haiku; - if (model.includes('gpt-4o')) return GPT-4o; - if (model.includes('gpt')) return GPT; - return {model.split('-')[0]}; - })() - ) : API} -

    - {request.routedModel && request.routedModel !== request.originalModel && ( - - - routed - - )} - {request.response?.statusCode && ( - = 200 && request.response.statusCode < 300 - ? 'bg-green-100 text-green-700' - : request.response.statusCode >= 300 && request.response.statusCode < 400 - ? 'bg-yellow-100 text-yellow-700' - : 'bg-red-100 text-red-700' - }`}> - {request.response.statusCode} - - )} - {request.conversationId && ( - - Turn {request.turnNumber} - - )} -
    - - {/* Endpoint */} -
    - {getChatCompletionsEndpoint(request.routedModel, request.endpoint)} -
    - - {/* Metrics Row */} -
    - {request.response?.body?.usage && ( - <> - - {((request.response.body.usage.input_tokens || 0) + (request.response.body.usage.output_tokens || 0)).toLocaleString()} tokens - - {request.response.body.usage.cache_read_input_tokens && ( - - {request.response.body.usage.cache_read_input_tokens.toLocaleString()} cached - - )} - - )} - - {request.response?.responseTime && ( - - {(request.response.responseTime / 1000).toFixed(2)}s - - )} -
    -
    -
    -
    - {new Date(request.timestamp).toLocaleDateString()} -
    -
    - {new Date(request.timestamp).toLocaleTimeString()} -
    -
    -
    -
    - ))} - {hasMoreRequests && ( -
    - -
    - )} - - )} -
    -
    - ) : ( - /* Conversations View */ -
    -
    -

    Conversations

    -
    -
    - {(isFetching && conversationsCurrentPage === 1) || isPending ? ( -
    - -

    Loading conversations...

    -
    - ) : conversations.length === 0 ? ( -
    -

    No conversations found

    -

    Start a conversation to see it appear here

    -
    - ) : ( - <> - {conversations.map(conversation => ( -
    loadConversationDetails(conversation.id, conversation.projectName)}> -
    -
    -
    - - #{conversation.id.slice(-8)} - - - {conversation.requestCount} turns - - - {formatDuration(conversation.duration)} - - {conversation.projectName && ( - - {conversation.projectName} - - )} -
    -
    -
    -
    First Message
    -
    - {conversation.firstMessage || "No content"} -
    -
    - {conversation.lastMessage && conversation.lastMessage !== conversation.firstMessage && ( -
    -
    Latest Message
    -
    - {conversation.lastMessage} -
    -
    - )} -
    -
    -
    -
    - {new Date(conversation.startTime).toLocaleDateString()} -
    -
    - {new Date(conversation.startTime).toLocaleTimeString()} -
    -
    -
    -
    - ))} - {hasMoreConversations && ( -
    - -
    - )} - - )} -
    -
    - )} -
    - - {/* Request Detail Modal */} - {isModalOpen && selectedRequest && ( -
    -
    -
    -
    -
    - -

    Request Details

    -
    - -
    -
    -
    - gradeRequest(selectedRequest.id)} /> -
    -
    -
    - )} - - {/* Conversation Detail Modal */} - {isConversationModalOpen && selectedConversation && ( -
    -
    -
    -
    -
    - -

    - Conversation {selectedConversation.sessionId.slice(-8)} -

    - - {selectedConversation.messageCount} messages - -
    - -
    -
    -
    -
    - {/* Conversation Overview */} -
    -
    -
    -
    {selectedConversation.messageCount}
    -
    Messages
    -
    -
    -
    {new Date(selectedConversation.startTime).toLocaleDateString()}
    -
    Started
    -
    -
    -
    {new Date(selectedConversation.endTime).toLocaleDateString()}
    -
    Last Activity
    -
    -
    -
    - - {/* Conversation Thread */} - -
    -
    -
    -
    - )} -
    - ); -} diff --git a/web/app/routes/api.conversations.tsx b/web/app/routes/api.conversations.tsx deleted file mode 100644 index 988a84c..0000000 --- a/web/app/routes/api.conversations.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { LoaderFunction } from "@remix-run/node"; -import { json } from "@remix-run/node"; - -export const loader: LoaderFunction = async ({ request }) => { - try { - const url = new URL(request.url); - const modelFilter = url.searchParams.get("model"); - - const backendUrl = new URL('http://localhost:3001/api/conversations'); - if (modelFilter) { - backendUrl.searchParams.append('model', modelFilter); - } - - const response = await fetch(backendUrl.toString()); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - return json(data); - } catch (error) { - console.error('Failed to fetch conversations:', error); - return json({ conversations: [] }); - } -}; \ No newline at end of file diff --git a/web/app/routes/api.grade-prompt.tsx b/web/app/routes/api.grade-prompt.tsx deleted file mode 100644 index 4a84253..0000000 --- a/web/app/routes/api.grade-prompt.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { ActionFunction } from "@remix-run/node"; -import { json } from "@remix-run/node"; - -export const action: ActionFunction = async ({ request }) => { - if (request.method !== "POST") { - return json({ error: 'Method not allowed' }, { status: 405 }); - } - - try { - const body = await request.json(); - - // Forward the request to the Go backend - const response = await fetch('http://localhost:3001/api/grade-prompt', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(body) - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - return json(data); - } catch (error) { - console.error('Failed to grade prompt:', error); - return json({ - error: 'Failed to grade prompt. Please ensure the backend is running and has a valid Anthropic API key.' - }, { status: 500 }); - } -}; \ No newline at end of file diff --git a/web/app/routes/api.requests.tsx b/web/app/routes/api.requests.tsx deleted file mode 100644 index 7f2b9cf..0000000 --- a/web/app/routes/api.requests.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type { ActionFunction, LoaderFunction } from "@remix-run/node"; -import { json } from "@remix-run/node"; - -export const loader: LoaderFunction = async ({ request }) => { - try { - const url = new URL(request.url); - const modelFilter = url.searchParams.get("model"); - const page = url.searchParams.get("page"); - const limit = url.searchParams.get("limit"); - - // Forward the request to the Go backend - const backendUrl = new URL('http://localhost:3001/api/requests'); - if (modelFilter) { - backendUrl.searchParams.append('model', modelFilter); - } - if (page) { - backendUrl.searchParams.append('page', page); - } - if (limit) { - backendUrl.searchParams.append('limit', limit); - } - - const response = await fetch(backendUrl.toString()); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - return json(data); - } catch (error) { - console.error('Failed to fetch requests:', error); - - // Return empty array if backend is not available - return json({ requests: [] }); - } -}; - -export const action: ActionFunction = async ({ request }) => { - const method = request.method; - - if (method === "DELETE") { - try { - // Forward the DELETE request to the Go backend - const response = await fetch('http://localhost:3001/api/requests', { - method: 'DELETE' - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return json({ success: true }); - } catch (error) { - console.error('Failed to clear requests:', error); - return json({ success: false, error: 'Failed to clear requests' }, { status: 500 }); - } - } - - return json({ error: 'Method not allowed' }, { status: 405 }); -}; \ No newline at end of file diff --git a/web/app/tailwind.css b/web/app/tailwind.css deleted file mode 100644 index 7374977..0000000 --- a/web/app/tailwind.css +++ /dev/null @@ -1,58 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -* { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; -} - -body { - background-color: #fafafa; - color: #111; -} - -@layer utilities { - .line-clamp-2 { - overflow: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - } -} - - - -.code-block { - font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace; - font-size: 0.75rem; - line-height: 1.5; - background: #f5f5f5; - border: 1px solid #e5e5e5; - border-radius: 4px; -} - - -.scrollbar-custom { - scrollbar-width: thin; - scrollbar-color: #ddd #f5f5f5; -} - -.scrollbar-custom::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -.scrollbar-custom::-webkit-scrollbar-track { - background: #f5f5f5; - border-radius: 3px; -} - -.scrollbar-custom::-webkit-scrollbar-thumb { - background: #ddd; - border-radius: 3px; -} - -.scrollbar-custom::-webkit-scrollbar-thumb:hover { - background: #ccc; -} - diff --git a/web/app/utils/formatters.ts b/web/app/utils/formatters.ts deleted file mode 100644 index 4b02e5a..0000000 --- a/web/app/utils/formatters.ts +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Utility functions for formatting and displaying data - */ - -/** - * Safely converts any value to a formatted string for display - */ -export function formatValue(value: any): string { - if (value === null) return 'null'; - if (value === undefined) return 'undefined'; - if (typeof value === 'string') return value; - if (typeof value === 'number' || typeof value === 'boolean') return String(value); - - try { - return JSON.stringify(value, null, 2); - } catch (error) { - return String(value); - } -} - -/** - * Formats JSON with proper indentation and returns a formatted string - */ -export function formatJSON(obj: any, maxLength: number = 1000): string { - try { - const jsonString = JSON.stringify(obj, null, 2); - if (jsonString.length > maxLength) { - return jsonString.substring(0, maxLength) + '...'; - } - return jsonString; - } catch (error) { - return String(obj); - } -} - -/** - * Escapes HTML characters to prevent XSS - */ -export function escapeHtml(text: string): string { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -/** - * Formats large text with proper line breaks and structure, optimized for the new conversation flow - */ -export function formatLargeText(text: string): string { - if (!text) return ''; - - // Escape HTML first - const escaped = escapeHtml(text); - - // Format the text with proper spacing and structure - return escaped - // Preserve existing double line breaks - .replace(/\n\n/g, '

    ') - // Convert single line breaks to single
    tags - .replace(/\n/g, '
    ') - // Format bullet points with modern styling - .replace(/^(\s*)([-*•])\s+(.+)$/gm, '$1$3') - // Format numbered lists with modern styling - .replace(/^(\s*)(\d+)\.\s+(.+)$/gm, '$1$2$3') - // Format headers with better typography - .replace(/^([A-Z][^<\n]*:)(
    |$)/gm, '
    $1
    $2') - // Format code blocks with better styling - .replace(/\b([A-Z_]{3,})\b/g, '$1') - // Format file paths and technical terms - .replace(/\b([a-zA-Z0-9_-]+\.[a-zA-Z]{2,4})\b/g, '$1') - // Format URLs with modern link styling - .replace(/(https?:\/\/[^\s<]+)/g, '$1') - // Format quoted text - .replace(/^(\s*)([""](.+?)[""])/gm, '$1
    $3
    ') - // Add proper spacing around paragraphs - .replace(/(

    )/g, '
    ') - // Clean up any excessive spacing - .replace(/(
    \s*){3,}/g, '

    ') - // Format emphasis patterns - .replace(/\*\*([^*]+)\*\*/g, '$1') - .replace(/\*([^*]+)\*/g, '$1') - // Format inline code - .replace(/`([^`]+)`/g, '$1'); -} - -/** - * Determines if a value is a complex object that should be JSON-formatted - */ -export function isComplexObject(value: any): boolean { - return value !== null && - typeof value === 'object' && - !Array.isArray(value) && - Object.keys(value).length > 0; -} - -/** - * Truncates text to a specified length with ellipsis - */ -export function truncateText(text: string, maxLength: number = 200): string { - if (text.length <= maxLength) return text; - return text.substring(0, maxLength) + '...'; -} - -/** - * Formats timestamp for display in the conversation flow - */ -export function formatTimestamp(timestamp: string | Date): string { - try { - const date = new Date(timestamp); - const now = new Date(); - const diff = now.getTime() - date.getTime(); - - // Less than a minute ago - if (diff < 60000) { - return 'Just now'; - } - - // Less than an hour ago - if (diff < 3600000) { - const minutes = Math.floor(diff / 60000); - return `${minutes}m ago`; - } - - // Less than a day ago - if (diff < 86400000) { - const hours = Math.floor(diff / 3600000); - return `${hours}h ago`; - } - - // More than a day ago - show time - return date.toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - hour12: true - }); - } catch { - return String(timestamp); - } -} - -/** - * Formats file size for display - */ -export function formatFileSize(bytes: number): string { - const sizes = ['B', 'KB', 'MB', 'GB']; - if (bytes === 0) return '0 B'; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; -} - -/** - * Creates a content preview for message summaries - */ -export function createContentPreview(content: any, maxLength: number = 100): string { - if (typeof content === 'string') { - return content.length > maxLength ? content.substring(0, maxLength) + '...' : content; - } - - if (Array.isArray(content)) { - const textContent = content.find(c => c.type === 'text')?.text || ''; - if (textContent) { - return textContent.length > maxLength ? textContent.substring(0, maxLength) + '...' : textContent; - } - return `${content.length} content blocks`; - } - - if (content && typeof content === 'object') { - if (content.text) { - return content.text.length > maxLength ? content.text.substring(0, maxLength) + '...' : content.text; - } - return 'Complex content'; - } - - return 'No content'; -} \ No newline at end of file diff --git a/web/app/utils/models.ts b/web/app/utils/models.ts deleted file mode 100644 index 33d1d79..0000000 --- a/web/app/utils/models.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Utility functions for model-related operations - */ - -/** - * Checks if a model is an OpenAI model - * @param model - The model name to check - * @returns true if the model is an OpenAI model - */ -export function isOpenAIModel(model: string | null | undefined): boolean { - if (!model) return false; - return model.startsWith('gpt-') || model.startsWith('o'); -} - -/** - * Gets the provider name based on the model - * @param model - The model name - * @returns 'OpenAI' for OpenAI models, 'Anthropic' otherwise - */ -export function getProviderName(model: string | null | undefined): 'OpenAI' | 'Anthropic' { - return isOpenAIModel(model) ? 'OpenAI' : 'Anthropic'; -} - -/** - * Gets the appropriate chat completions endpoint based on the model - * @param model - The model name - * @param defaultEndpoint - The default endpoint to use for non-OpenAI models - * @returns The appropriate endpoint - */ -export function getChatCompletionsEndpoint(model: string | null | undefined, defaultEndpoint?: string): string { - return isOpenAIModel(model) ? '/v1/chat/completions' : (defaultEndpoint || '/v1/messages'); -} \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json deleted file mode 100644 index 97e7738..0000000 --- a/web/package-lock.json +++ /dev/null @@ -1,13468 +0,0 @@ -{ - "name": "web", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "web", - "dependencies": { - "@remix-run/node": "^2.16.8", - "@remix-run/react": "^2.16.8", - "@remix-run/serve": "^2.16.8", - "isbot": "^4.1.0", - "lucide-react": "^0.522.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@remix-run/dev": "^2.16.8", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", - "autoprefixer": "^10.4.19", - "eslint": "^8.38.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "postcss": "^8.4.38", - "tailwindcss": "^3.4.4", - "typescript": "^5.1.6", - "vite": "^6.0.0", - "vite-tsconfig-paths": "^4.2.1" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", - "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", - "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.27.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", - "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz", - "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", - "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", - "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", - "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.6.tgz", - "integrity": "sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz", - "integrity": "sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.6.tgz", - "integrity": "sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.6.tgz", - "integrity": "sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz", - "integrity": "sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz", - "integrity": "sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz", - "integrity": "sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz", - "integrity": "sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz", - "integrity": "sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz", - "integrity": "sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz", - "integrity": "sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz", - "integrity": "sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz", - "integrity": "sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz", - "integrity": "sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz", - "integrity": "sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz", - "integrity": "sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz", - "integrity": "sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz", - "integrity": "sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz", - "integrity": "sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz", - "integrity": "sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz", - "integrity": "sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz", - "integrity": "sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jspm/core": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@jspm/core/-/core-2.1.0.tgz", - "integrity": "sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@mdx-js/mdx": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz", - "integrity": "sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/mdx": "^2.0.0", - "estree-util-build-jsx": "^2.0.0", - "estree-util-is-identifier-name": "^2.0.0", - "estree-util-to-js": "^1.1.0", - "estree-walker": "^3.0.0", - "hast-util-to-estree": "^2.0.0", - "markdown-extensions": "^1.0.0", - "periscopic": "^3.0.0", - "remark-mdx": "^2.0.0", - "remark-parse": "^10.0.0", - "remark-rehype": "^10.0.0", - "unified": "^10.0.0", - "unist-util-position-from-estree": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "unist-util-visit": "^4.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", - "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.4.0" - } - }, - "node_modules/@npmcli/fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", - "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", - "dev": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/git": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", - "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/promise-spawn": "^6.0.0", - "lru-cache": "^7.4.4", - "npm-pick-manifest": "^8.0.0", - "proc-log": "^3.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/@npmcli/package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^4.1.0", - "glob": "^10.2.2", - "hosted-git-info": "^6.1.1", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^5.0.0", - "proc-log": "^3.0.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/promise-spawn": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", - "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", - "dev": true, - "license": "ISC", - "dependencies": { - "which": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@remix-run/dev": { - "version": "2.16.8", - "resolved": "https://registry.npmjs.org/@remix-run/dev/-/dev-2.16.8.tgz", - "integrity": "sha512-2EKByaD5CDwh7H56UFVCqc90kCZ9LukPlSwkcsR3gj7WlfL7sXtcIqIopcToAlKAeao3HDbhBlBT2CTOivxZCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.8", - "@babel/generator": "^7.21.5", - "@babel/parser": "^7.21.8", - "@babel/plugin-syntax-decorators": "^7.22.10", - "@babel/plugin-syntax-jsx": "^7.21.4", - "@babel/preset-typescript": "^7.21.5", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.22.5", - "@mdx-js/mdx": "^2.3.0", - "@npmcli/package-json": "^4.0.1", - "@remix-run/node": "2.16.8", - "@remix-run/router": "1.23.0", - "@remix-run/server-runtime": "2.16.8", - "@types/mdx": "^2.0.5", - "@vanilla-extract/integration": "^6.2.0", - "arg": "^5.0.1", - "cacache": "^17.1.3", - "chalk": "^4.1.2", - "chokidar": "^3.5.1", - "cross-spawn": "^7.0.3", - "dotenv": "^16.0.0", - "es-module-lexer": "^1.3.1", - "esbuild": "0.17.6", - "esbuild-plugins-node-modules-polyfill": "^1.6.0", - "execa": "5.1.1", - "exit-hook": "2.2.1", - "express": "^4.20.0", - "fs-extra": "^10.0.0", - "get-port": "^5.1.1", - "gunzip-maybe": "^1.4.2", - "jsesc": "3.0.2", - "json5": "^2.2.2", - "lodash": "^4.17.21", - "lodash.debounce": "^4.0.8", - "minimatch": "^9.0.0", - "ora": "^5.4.1", - "pathe": "^1.1.2", - "picocolors": "^1.0.0", - "picomatch": "^2.3.1", - "pidtree": "^0.6.0", - "postcss": "^8.4.19", - "postcss-discard-duplicates": "^5.1.0", - "postcss-load-config": "^4.0.1", - "postcss-modules": "^6.0.0", - "prettier": "^2.7.1", - "pretty-ms": "^7.0.1", - "react-refresh": "^0.14.0", - "remark-frontmatter": "4.0.1", - "remark-mdx-frontmatter": "^1.0.1", - "semver": "^7.3.7", - "set-cookie-parser": "^2.6.0", - "tar-fs": "^2.1.3", - "tsconfig-paths": "^4.0.0", - "valibot": "^0.41.0", - "vite-node": "^3.1.3", - "ws": "^7.5.10" - }, - "bin": { - "remix": "dist/cli.js" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@remix-run/react": "^2.16.8", - "@remix-run/serve": "^2.16.8", - "typescript": "^5.1.0", - "vite": "^5.1.0 || ^6.0.0", - "wrangler": "^3.28.2" - }, - "peerDependenciesMeta": { - "@remix-run/serve": { - "optional": true - }, - "typescript": { - "optional": true - }, - "vite": { - "optional": true - }, - "wrangler": { - "optional": true - } - } - }, - "node_modules/@remix-run/express": { - "version": "2.16.8", - "resolved": "https://registry.npmjs.org/@remix-run/express/-/express-2.16.8.tgz", - "integrity": "sha512-NNTosiAJ4jZCRDfWSjV+3Fyu7KoHPeEHruLZEPRNDuXO6Nm5EkRvIkMwdfwyJ+ajE5IPotu8MFtPyNtm3sw/gw==", - "license": "MIT", - "dependencies": { - "@remix-run/node": "2.16.8" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "express": "^4.20.0", - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@remix-run/node": { - "version": "2.16.8", - "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.16.8.tgz", - "integrity": "sha512-foeYXU3mdaBJZnbtGbM8mNdHowz2+QnVGDRo7P3zgFkmsccMEflArGZNbkACGKd9xwDguTxxMJ6cuXBC4jIfgQ==", - "license": "MIT", - "dependencies": { - "@remix-run/server-runtime": "2.16.8", - "@remix-run/web-fetch": "^4.4.2", - "@web3-storage/multipart-parser": "^1.0.0", - "cookie-signature": "^1.1.0", - "source-map-support": "^0.5.21", - "stream-slice": "^0.1.2", - "undici": "^6.21.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@remix-run/react": { - "version": "2.16.8", - "resolved": "https://registry.npmjs.org/@remix-run/react/-/react-2.16.8.tgz", - "integrity": "sha512-JmoBUnEu/nPLkU6NGNIG7rfLM97gPpr1LYRJeV680hChr0/2UpfQQwcRLtHz03w1Gz1i/xONAAVOvRHVcXkRlA==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0", - "@remix-run/server-runtime": "2.16.8", - "react-router": "6.30.0", - "react-router-dom": "6.30.0", - "turbo-stream": "2.4.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0", - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@remix-run/serve": { - "version": "2.16.8", - "resolved": "https://registry.npmjs.org/@remix-run/serve/-/serve-2.16.8.tgz", - "integrity": "sha512-4exyeXCZoc/Vo8Zc+6Eyao3ONwOyNOK3Yeb0LLkWXd4aeFQ4v59i5fq/j/E+68UnpD/UZQl1Bj0k2hQnGQZhlQ==", - "license": "MIT", - "dependencies": { - "@remix-run/express": "2.16.8", - "@remix-run/node": "2.16.8", - "chokidar": "^3.5.3", - "compression": "^1.7.4", - "express": "^4.20.0", - "get-port": "5.1.1", - "morgan": "^1.10.0", - "source-map-support": "^0.5.21" - }, - "bin": { - "remix-serve": "dist/cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@remix-run/server-runtime": { - "version": "2.16.8", - "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.16.8.tgz", - "integrity": "sha512-ZwWOam4GAQTx10t+wK09YuYctd2Koz5Xy/klDgUN3lmTXmwbV0tZU0baiXEqZXrvyD+WDZ4b0ADDW9Df3+dpzA==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0", - "@types/cookie": "^0.6.0", - "@web3-storage/multipart-parser": "^1.0.0", - "cookie": "^0.7.2", - "set-cookie-parser": "^2.4.8", - "source-map": "^0.7.3", - "turbo-stream": "2.4.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@remix-run/web-blob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@remix-run/web-blob/-/web-blob-3.1.0.tgz", - "integrity": "sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==", - "license": "MIT", - "dependencies": { - "@remix-run/web-stream": "^1.1.0", - "web-encoding": "1.1.5" - } - }, - "node_modules/@remix-run/web-fetch": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@remix-run/web-fetch/-/web-fetch-4.4.2.tgz", - "integrity": "sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==", - "license": "MIT", - "dependencies": { - "@remix-run/web-blob": "^3.1.0", - "@remix-run/web-file": "^3.1.0", - "@remix-run/web-form-data": "^3.1.0", - "@remix-run/web-stream": "^1.1.0", - "@web3-storage/multipart-parser": "^1.0.0", - "abort-controller": "^3.0.0", - "data-uri-to-buffer": "^3.0.1", - "mrmime": "^1.0.0" - }, - "engines": { - "node": "^10.17 || >=12.3" - } - }, - "node_modules/@remix-run/web-file": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@remix-run/web-file/-/web-file-3.1.0.tgz", - "integrity": "sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==", - "license": "MIT", - "dependencies": { - "@remix-run/web-blob": "^3.1.0" - } - }, - "node_modules/@remix-run/web-form-data": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@remix-run/web-form-data/-/web-form-data-3.1.0.tgz", - "integrity": "sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==", - "license": "MIT", - "dependencies": { - "web-encoding": "1.1.5" - } - }, - "node_modules/@remix-run/web-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@remix-run/web-stream/-/web-stream-1.1.0.tgz", - "integrity": "sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==", - "license": "MIT", - "dependencies": { - "web-streams-polyfill": "^3.1.1" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", - "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz", - "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", - "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", - "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz", - "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz", - "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz", - "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz", - "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", - "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", - "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz", - "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz", - "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz", - "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz", - "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz", - "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", - "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", - "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", - "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz", - "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", - "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", - "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/acorn": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", - "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT" - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "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/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/@types/mdx": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", - "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", - "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.23", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", - "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.1.tgz", - "integrity": "sha512-dd7yIp1hfJFX9ZlVLQRrh/Re9WMUHHmF9hrKD1yIvxcyNr2BhQ3xc1upAVhy8NijadnCswAxWQu8MkkSMC1qXQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.1.tgz", - "integrity": "sha512-EzUPcMFtDVlo5yrbzMqUsGq3HnLXw+3ZOhSd7CUaDmbTtnrzM+RO2ntw2dm2wjbbc5djWj3yX0wzbbg8pLhx8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.1.tgz", - "integrity": "sha512-nB+dna3q4kOleKFcSZJ/wDXIsAd1kpMO9XrVAt8tG3RDWJ6vi+Ic6bpz4cmg5tWNeCfHEY4KuqJCB+pKejPEmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.1.tgz", - "integrity": "sha512-aKWHCrOGaCGwZcekf3TnczQoBxk5w//W3RZ4EQyhux6rKDwBPgDU9Y2yGigCV1Z+8DWqZgVGQi+hdpnlSy3a1w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.1.tgz", - "integrity": "sha512-4dIEMXrXt0UqDVgrsUd1I+NoIzVQWXy/CNhgpfS75rOOMK/4Abn0Mx2M2gWH4Mk9+ds/ASAiCmqoUFynmMY5hA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.1.tgz", - "integrity": "sha512-vtvS13IXPs1eE8DuS/soiosqMBeyh50YLRZ+p7EaIKAPPeevRnA9G/wu/KbVt01ZD5qiGjxS+CGIdVC7I6gTOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.1.tgz", - "integrity": "sha512-BfdnN6aZ7NcX8djW8SR6GOJc+K+sFhWRF4vJueVE0vbUu5N1bLnBpxJg1TGlhSyo+ImC4SR0jcNiKN0jdoxt+A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.1.tgz", - "integrity": "sha512-Jhge7lFtH0QqfRz2PyJjJXWENqywPteITd+nOS0L6AhbZli+UmEyGBd2Sstt1c+l9C+j/YvKTl9wJo9PPmsFNg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.1.tgz", - "integrity": "sha512-ofdK/ow+ZSbSU0pRoB7uBaiRHeaAOYQFU5Spp87LdcPL/P1RhbCTMSIYVb61XWzsVEmYKjHFtoIE0wxP6AFvrA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.1.tgz", - "integrity": "sha512-eC8SXVn8de67HacqU7PoGdHA+9tGbqfEdD05AEFRAB81ejeQtNi5Fx7lPcxpLH79DW0BnMAHau3hi4RVkHfSCw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.1.tgz", - "integrity": "sha512-fIkwvAAQ41kfoGWfzeJ33iLGShl0JEDZHrMnwTHMErUcPkaaZRJYjQjsFhMl315NEQ4mmTlC+2nfK/J2IszDOw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.1.tgz", - "integrity": "sha512-RAAszxImSOFLk44aLwnSqpcOdce8sBcxASledSzuFAd8Q5ZhhVck472SisspnzHdc7THCvGXiUeZ2hOC7NUoBQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.1.tgz", - "integrity": "sha512-QoP9vkY+THuQdZi05bA6s6XwFd6HIz3qlx82v9bTOgxeqin/3C12Ye7f7EOD00RQ36OtOPWnhEMMm84sv7d1XQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.1.tgz", - "integrity": "sha512-/p77cGN/h9zbsfCseAP5gY7tK+7+DdM8fkPfr9d1ye1fsF6bmtGbtZN6e/8j4jCZ9NEIBBkT0GhdgixSelTK9g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.1.tgz", - "integrity": "sha512-wInTqT3Bu9u50mDStEig1v8uxEL2Ht+K8pir/YhyyrM5ordJtxoqzsL1vR/CQzOJuDunUTrDkMM0apjW/d7/PA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.1.tgz", - "integrity": "sha512-eNwqO5kUa+1k7yFIircwwiniKWA0UFHo2Cfm8LYgkh9km7uMad+0x7X7oXbQonJXlqfitBTSjhA0un+DsHIrhw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.1.tgz", - "integrity": "sha512-Eaz1xMUnoa2mFqh20mPqSdbYl6crnk8HnIXDu6nsla9zpgZJZO8w3c1gvNN/4Eb0RXRq3K9OG6mu8vw14gIqiA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.1.tgz", - "integrity": "sha512-H/+d+5BGlnEQif0gnwWmYbYv7HJj563PUKJfn8PlmzF8UmF+8KxdvXdwCsoOqh4HHnENnoLrav9NYBrv76x1wQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.1.tgz", - "integrity": "sha512-rS86wI4R6cknYM3is3grCb/laE8XBEbpWAMSIPjYfmYp75KL5dT87jXF2orDa4tQYg5aajP5G8Fgh34dRyR+Rw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vanilla-extract/babel-plugin-debug-ids": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.2.2.tgz", - "integrity": "sha512-MeDWGICAF9zA/OZLOKwhoRlsUW+fiMwnfuOAqFVohL31Agj7Q/RBWAYweqjHLgFBCsdnr6XIfwjJnmb2znEWxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.9" - } - }, - "node_modules/@vanilla-extract/css": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.17.4.tgz", - "integrity": "sha512-m3g9nQDWPtL+sTFdtCGRMI1Vrp86Ay4PBYq1Bo7Bnchj5ElNtAJpOqD+zg+apthVA4fB7oVpMWNjwpa6ElDWFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.0", - "@vanilla-extract/private": "^1.0.9", - "css-what": "^6.1.0", - "cssesc": "^3.0.0", - "csstype": "^3.0.7", - "dedent": "^1.5.3", - "deep-object-diff": "^1.1.9", - "deepmerge": "^4.2.2", - "lru-cache": "^10.4.3", - "media-query-parser": "^2.0.2", - "modern-ahocorasick": "^1.0.0", - "picocolors": "^1.0.0" - } - }, - "node_modules/@vanilla-extract/css/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@vanilla-extract/integration": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-6.5.0.tgz", - "integrity": "sha512-E2YcfO8vA+vs+ua+gpvy1HRqvgWbI+MTlUpxA8FvatOvybuNcWAY0CKwQ/Gpj7rswYKtC6C7+xw33emM6/ImdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.20.7", - "@babel/plugin-syntax-typescript": "^7.20.0", - "@vanilla-extract/babel-plugin-debug-ids": "^1.0.4", - "@vanilla-extract/css": "^1.14.0", - "esbuild": "npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0", - "eval": "0.1.8", - "find-up": "^5.0.0", - "javascript-stringify": "^2.0.1", - "lodash": "^4.17.21", - "mlly": "^1.4.2", - "outdent": "^0.8.0", - "vite": "^5.0.11", - "vite-node": "^1.2.0" - } - }, - "node_modules/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/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/@vanilla-extract/integration/node_modules/vite": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", - "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", - "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/@vanilla-extract/integration/node_modules/vite-node": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", - "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "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/@vanilla-extract/integration/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/@vanilla-extract/private": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.9.tgz", - "integrity": "sha512-gT2jbfZuaaCLrAxwXbRgIhGhcXbRZCG3v4TTUnjw0EJ7ArdBRxkq4msNJkbuRkCgfIK5ATmprB5t9ljvLeFDEA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@web3-storage/multipart-parser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz", - "integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==", - "license": "(Apache-2.0 AND MIT)" - }, - "node_modules/@zxing/text-encoding": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", - "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", - "license": "(Unlicense OR Apache-2.0)", - "optional": true - }, - "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/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/astring": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", - "dev": true, - "license": "MIT", - "bin": { - "astring": "bin/astring" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "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==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserify-zlib": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", - "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pako": "~0.2.0" - } - }, - "node_modules/browserslist": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", - "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001718", - "electron-to-chromium": "^1.5.160", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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/cacache": { - "version": "17.1.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", - "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^7.7.1", - "minipass": "^7.0.3", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001724", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz", - "integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", - "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.0.2", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/data-uri-to-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", - "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", - "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deep-object-diff": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", - "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "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/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/duplexify/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/duplexify/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexify/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/duplexify/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.171", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz", - "integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.6.tgz", - "integrity": "sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.17.6", - "@esbuild/android-arm64": "0.17.6", - "@esbuild/android-x64": "0.17.6", - "@esbuild/darwin-arm64": "0.17.6", - "@esbuild/darwin-x64": "0.17.6", - "@esbuild/freebsd-arm64": "0.17.6", - "@esbuild/freebsd-x64": "0.17.6", - "@esbuild/linux-arm": "0.17.6", - "@esbuild/linux-arm64": "0.17.6", - "@esbuild/linux-ia32": "0.17.6", - "@esbuild/linux-loong64": "0.17.6", - "@esbuild/linux-mips64el": "0.17.6", - "@esbuild/linux-ppc64": "0.17.6", - "@esbuild/linux-riscv64": "0.17.6", - "@esbuild/linux-s390x": "0.17.6", - "@esbuild/linux-x64": "0.17.6", - "@esbuild/netbsd-x64": "0.17.6", - "@esbuild/openbsd-x64": "0.17.6", - "@esbuild/sunos-x64": "0.17.6", - "@esbuild/win32-arm64": "0.17.6", - "@esbuild/win32-ia32": "0.17.6", - "@esbuild/win32-x64": "0.17.6" - } - }, - "node_modules/esbuild-plugins-node-modules-polyfill": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/esbuild-plugins-node-modules-polyfill/-/esbuild-plugins-node-modules-polyfill-1.7.1.tgz", - "integrity": "sha512-IEaUhaS1RukGGcatDzqJmR+AzUWJ2upTJZP2i7FzR37Iw5Lk0LReCTnWnPMWnGG9lO4MWTGKEGGLWEOPegTRJA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jspm/core": "^2.1.0", - "local-pkg": "^1.1.1", - "resolve.exports": "^2.0.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.14.0 <=0.25.x" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-util-attach-comments": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-2.1.1.tgz", - "integrity": "sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-build-jsx": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-2.2.2.tgz", - "integrity": "sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "estree-util-is-identifier-name": "^2.0.0", - "estree-walker": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz", - "integrity": "sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-to-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz", - "integrity": "sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "astring": "^1.8.0", - "source-map": "^0.7.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-value-to-estree": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-1.3.0.tgz", - "integrity": "sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-obj": "^3.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/estree-util-visit": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-1.2.1.tgz", - "integrity": "sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "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/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eval": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", - "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "require-like": ">= 0.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "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/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit-hook": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", - "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fault": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", - "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "format": "^0.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "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/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generic-names": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz", - "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^3.2.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "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-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true, - "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/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/gunzip-maybe": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", - "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserify-zlib": "^0.1.4", - "is-deflate": "^1.0.0", - "is-gzip": "^1.0.0", - "peek-stream": "^1.1.0", - "pumpify": "^1.3.3", - "through2": "^2.0.3" - }, - "bin": { - "gunzip-maybe": "bin.js" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "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/hast-util-to-estree": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz", - "integrity": "sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "estree-util-attach-comments": "^2.0.0", - "estree-util-is-identifier-name": "^2.0.0", - "hast-util-whitespace": "^2.0.0", - "mdast-util-mdx-expression": "^1.0.0", - "mdast-util-mdxjs-esm": "^1.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.1", - "unist-util-position": "^4.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", - "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hosted-git-info": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz", - "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.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==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "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/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.7.1" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-deflate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", - "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-gzip": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", - "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isbot": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/isbot/-/isbot-4.4.0.tgz", - "integrity": "sha512-8ZvOWUA68kyJO4hHJdWjyreq7TYNWTS9y15IzeqVdKxR9pPr3P/3r9AHcoIv9M0Rllkao5qWz2v1lmcyKIVCzQ==", - "license": "Unlicense", - "engines": { - "node": ">=18" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/javascript-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", - "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", - "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/local-pkg": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz", - "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.0.1", - "quansync": "^0.2.8" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.522.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.522.0.tgz", - "integrity": "sha512-jnJbw974yZ7rQHHEFKJOlWAefG3ATSCZHANZxIdx8Rk/16siuwjgA4fBULpXEAWx/RlTs3FzmKW/udWUuO0aRw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/markdown-extensions": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", - "integrity": "sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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/mdast-util-definitions": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", - "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", - "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-frontmatter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-1.0.1.tgz", - "integrity": "sha512-JjA2OjxRqAa8wEG8hloD0uTU0kdn8kbtOWpPP94NBkfAlbxn4S8gCGf/9DwFtEeGPXrDcNXdiDjVaRdUFqYokw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0", - "micromark-extension-frontmatter": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-2.0.1.tgz", - "integrity": "sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-mdx-expression": "^1.0.0", - "mdast-util-mdx-jsx": "^2.0.0", - "mdast-util-mdxjs-esm": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz", - "integrity": "sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.4.tgz", - "integrity": "sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "ccount": "^2.0.0", - "mdast-util-from-markdown": "^1.1.0", - "mdast-util-to-markdown": "^1.3.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-remove-position": "^4.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.1.tgz", - "integrity": "sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", - "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", - "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-definitions": "^5.0.0", - "micromark-util-sanitize-uri": "^1.1.0", - "trim-lines": "^3.0.0", - "unist-util-generated": "^2.0.0", - "unist-util-position": "^4.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", - "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/media-query-parser": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-2.0.2.tgz", - "integrity": "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromark": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", - "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", - "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-extension-frontmatter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-1.1.1.tgz", - "integrity": "sha512-m2UH9a7n3W8VAH9JO9y01APpPKmNNNs71P0RbknEmYSaZU5Ghogv38BYO94AI5Xw6OYfxZRdHZZ2nYjs/Z+SZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fault": "^2.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-expression": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.8.tgz", - "integrity": "sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "micromark-factory-mdx-expression": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.5.tgz", - "integrity": "sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/acorn": "^4.0.0", - "@types/estree": "^1.0.0", - "estree-util-is-identifier-name": "^2.0.0", - "micromark-factory-mdx-expression": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-md": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.1.tgz", - "integrity": "sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.1.tgz", - "integrity": "sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^1.0.0", - "micromark-extension-mdx-jsx": "^1.0.0", - "micromark-extension-mdx-md": "^1.0.0", - "micromark-extension-mdxjs-esm": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.5.tgz", - "integrity": "sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "micromark-core-commonmark": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-position-from-estree": "^1.1.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", - "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", - "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-factory-mdx-expression": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.9.tgz", - "integrity": "sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-events-to-acorn": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-position-from-estree": "^1.0.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", - "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", - "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", - "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", - "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", - "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", - "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-events-to-acorn": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz", - "integrity": "sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/acorn": "^4.0.0", - "@types/estree": "^1.0.0", - "@types/unist": "^2.0.0", - "estree-util-visit": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0", - "vfile-message": "^3.0.0" - } - }, - "node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", - "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", - "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", - "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "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/mime-types/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/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/mlly": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", - "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.14.0", - "pathe": "^2.0.1", - "pkg-types": "^1.3.0", - "ufo": "^1.5.4" - } - }, - "node_modules/mlly/node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mlly/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mlly/node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/modern-ahocorasick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/modern-ahocorasick/-/modern-ahocorasick-1.1.0.tgz", - "integrity": "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "license": "MIT", - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/morgan/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "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/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "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-postinstall": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.4.tgz", - "integrity": "sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-package-data": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", - "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^6.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-install-checks": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", - "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-package-arg": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", - "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^6.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-pick-manifest": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz", - "integrity": "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==", - "dev": true, - "license": "ISC", - "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^10.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/outdent": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz", - "integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==", - "dev": true, - "license": "MIT" - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", - "dev": true, - "license": "MIT" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", - "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/peek-stream": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", - "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "duplexify": "^3.5.0", - "through2": "^2.0.3" - } - }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-types": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz", - "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.2.1", - "exsolve": "^1.0.1", - "pathe": "^2.0.3" - } - }, - "node_modules/pkg-types/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "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/postcss-discard-duplicates": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-modules": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-6.0.1.tgz", - "integrity": "sha512-zyo2sAkVvuZFFy0gc2+4O+xar5dYlaVy/ebO24KT0ftk/iJevSNyPyQellsBLlnccwh7f6V6Y4GvuKRYToNgpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "generic-names": "^4.0.0", - "icss-utils": "^5.1.0", - "lodash.camelcase": "^4.3.0", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "string-hash": "^1.1.3" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-ms": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", - "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-ms": "^2.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quansync": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", - "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", - "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", - "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/remark-frontmatter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-4.0.1.tgz", - "integrity": "sha512-38fJrB0KnmD3E33a5jZC/5+gGAC2WKNiPw1/fdXJvijBlhA7RCsvJklrYJakS0HedninvaCYW8lQGf9C918GfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-frontmatter": "^1.0.0", - "micromark-extension-frontmatter": "^1.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-2.3.0.tgz", - "integrity": "sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdast-util-mdx": "^2.0.0", - "micromark-extension-mdxjs": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx-frontmatter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/remark-mdx-frontmatter/-/remark-mdx-frontmatter-1.1.1.tgz", - "integrity": "sha512-7teX9DW4tI2WZkXS4DBxneYSY7NHiXl4AKdWDO9LXVweULlCT8OPWsOjLEnMIXViN1j+QcY8mfbq3k0EK6x3uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "estree-util-is-identifier-name": "^1.0.0", - "estree-util-value-to-estree": "^1.0.0", - "js-yaml": "^4.0.0", - "toml": "^3.0.0" - }, - "engines": { - "node": ">=12.2.0" - } - }, - "node_modules/remark-mdx-frontmatter/node_modules/estree-util-is-identifier-name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-1.1.0.tgz", - "integrity": "sha512-OVJZ3fGGt9By77Ix9NhaRbzfbDV/2rx9EP7YIDJTmsZSEc5kYn2vWcNccYyahJL2uAQZK2a5Or2i0wtIKTPoRQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/remark-parse": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", - "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", - "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "mdast-util-to-hast": "^12.1.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/require-like": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", - "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "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/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/rollup": { - "version": "4.44.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", - "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==", - "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.44.0", - "@rollup/rollup-android-arm64": "4.44.0", - "@rollup/rollup-darwin-arm64": "4.44.0", - "@rollup/rollup-darwin-x64": "4.44.0", - "@rollup/rollup-freebsd-arm64": "4.44.0", - "@rollup/rollup-freebsd-x64": "4.44.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", - "@rollup/rollup-linux-arm-musleabihf": "4.44.0", - "@rollup/rollup-linux-arm64-gnu": "4.44.0", - "@rollup/rollup-linux-arm64-musl": "4.44.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", - "@rollup/rollup-linux-riscv64-gnu": "4.44.0", - "@rollup/rollup-linux-riscv64-musl": "4.44.0", - "@rollup/rollup-linux-s390x-gnu": "4.44.0", - "@rollup/rollup-linux-x64-gnu": "4.44.0", - "@rollup/rollup-linux-x64-musl": "4.44.0", - "@rollup/rollup-win32-arm64-msvc": "4.44.0", - "@rollup/rollup-win32-ia32-msvc": "4.44.0", - "@rollup/rollup-win32-x64-msvc": "4.44.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, - "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/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/ssri": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", - "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/stream-slice": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/stream-slice/-/stream-slice-0.1.2.tgz", - "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==", - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "dev": true, - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-to-object": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", - "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", - "dev": true, - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.1.1" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/tar-fs/node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tsconfck": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", - "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", - "dev": true, - "license": "MIT", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/turbo-stream": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.1.tgz", - "integrity": "sha512-v8kOJXpG3WoTN/+at8vK7erSzo6nW6CIaeOvNOkHQVDajfz1ZVeSxCbc6tOH4hrGZW7VUCV0TOXd8CPzYnYkrw==", - "license": "ISC" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unified/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/unist-util-generated": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", - "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", - "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position-from-estree": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz", - "integrity": "sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz", - "integrity": "sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unrs-resolver": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.1.tgz", - "integrity": "sha512-4AZVxP05JGN6DwqIkSP4VKLOcwQa5l37SWHF/ahcuqBMbfxbpN1L1QKafEhWCziHhzKex9H/AR09H0OuVyU+9g==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.2.2" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.9.1", - "@unrs/resolver-binding-android-arm64": "1.9.1", - "@unrs/resolver-binding-darwin-arm64": "1.9.1", - "@unrs/resolver-binding-darwin-x64": "1.9.1", - "@unrs/resolver-binding-freebsd-x64": "1.9.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.9.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.9.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.9.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.9.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.9.1", - "@unrs/resolver-binding-linux-x64-musl": "1.9.1", - "@unrs/resolver-binding-wasm32-wasi": "1.9.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.9.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.9.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.9.1" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uvu": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", - "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "bin": { - "uvu": "bin.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/valibot": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.41.0.tgz", - "integrity": "sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "typescript": ">=5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/validate-npm-package-name": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", - "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite-tsconfig-paths": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", - "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "globrex": "^0.1.2", - "tsconfck": "^3.0.3" - }, - "peerDependencies": { - "vite": "*" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/web-encoding": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", - "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", - "license": "MIT", - "dependencies": { - "util": "^0.12.3" - }, - "optionalDependencies": { - "@zxing/text-encoding": "0.9.0" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "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==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/web/package.json b/web/package.json deleted file mode 100644 index 4388d8f..0000000 --- a/web/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "web", - "private": true, - "sideEffects": false, - "type": "module", - "scripts": { - "build": "remix vite:build", - "dev": "remix vite:dev", - "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", - "start": "remix-serve ./build/server/index.js", - "typecheck": "tsc" - }, - "dependencies": { - "@remix-run/node": "^2.16.8", - "@remix-run/react": "^2.16.8", - "@remix-run/serve": "^2.16.8", - "isbot": "^4.1.0", - "lucide-react": "^0.522.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@remix-run/dev": "^2.16.8", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", - "autoprefixer": "^10.4.19", - "eslint": "^8.38.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "postcss": "^8.4.38", - "tailwindcss": "^3.4.4", - "typescript": "^5.1.6", - "vite": "^6.0.0", - "vite-tsconfig-paths": "^4.2.1" - }, - "engines": { - "node": ">=20.0.0" - } -} diff --git a/web/postcss.config.js b/web/postcss.config.js deleted file mode 100644 index 2aa7205..0000000 --- a/web/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/web/public/logo-dark.png b/web/public/logo-dark.png deleted file mode 100644 index b24c7ae..0000000 Binary files a/web/public/logo-dark.png and /dev/null differ diff --git a/web/public/logo-light.png b/web/public/logo-light.png deleted file mode 100644 index 4490ae7..0000000 Binary files a/web/public/logo-light.png and /dev/null differ diff --git a/web/tailwind.config.ts b/web/tailwind.config.ts deleted file mode 100644 index c95a09a..0000000 --- a/web/tailwind.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Config } from "tailwindcss"; - -export default { - content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], - theme: { - extend: { - fontFamily: { - sans: [ - "Inter", - "ui-sans-serif", - "system-ui", - "sans-serif", - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", - "Noto Color Emoji", - ], - }, - animation: { - spin: 'spin 1s linear infinite', - }, - keyframes: { - spin: { - from: { transform: 'rotate(0deg)' }, - to: { transform: 'rotate(360deg)' }, - }, - }, - }, - }, - plugins: [], -} satisfies Config; diff --git a/web/tsconfig.json b/web/tsconfig.json deleted file mode 100644 index 9d87dd3..0000000 --- a/web/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "include": [ - "**/*.ts", - "**/*.tsx", - "**/.server/**/*.ts", - "**/.server/**/*.tsx", - "**/.client/**/*.ts", - "**/.client/**/*.tsx" - ], - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": ["@remix-run/node", "vite/client"], - "isolatedModules": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "target": "ES2022", - "strict": true, - "allowJs": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "baseUrl": ".", - "paths": { - "~/*": ["./app/*"] - }, - - // Vite takes care of building everything, not tsc. - "noEmit": true - } -} diff --git a/web/vite.config.ts b/web/vite.config.ts deleted file mode 100644 index ef6d138..0000000 --- a/web/vite.config.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { vitePlugin as remix } from "@remix-run/dev"; -import { defineConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; - -declare module "@remix-run/node" { - interface Future { - v3_singleFetch: true; - } -} - -export default defineConfig({ - plugins: [ - remix({ - future: { - v3_fetcherPersist: true, - v3_relativeSplatPath: true, - v3_throwAbortReason: true, - v3_singleFetch: true, - v3_lazyRouteDiscovery: true, - }, - }), - tsconfigPaths(), - ], - server: { - proxy: { - "/api": { - target: "http://localhost:3001", - changeOrigin: true, - }, - }, - }, -});