From 8e550b9785aedb29f775e3ae342d5155370b5091 Mon Sep 17 00:00:00 2001 From: sid Date: Sat, 2 May 2026 15:15:58 -0600 Subject: [PATCH] Local fork: hardening + ops improvements (timeout knob, demotion, /livez, drain) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit captures both the prior accumulated work-in-progress (framework migration web/→svelte/, postgres storage, conversation viewer, dashboard auth, OpenAPI spec, integration tests) AND today's operational improvements layered on top. History wasn't checkpointed incrementally; happy to split it via interactive rebase if a reviewer wants smaller commits. Today's changes (in addition to the older WIP): 1. Configurable upstream response-header timeout - ANTHROPIC_RESPONSE_HEADER_TIMEOUT env (default 300s) - Replaces hardcoded 300s in provider/anthropic.go that was firing on opus + 1M-context + extended thinking non-streaming requests - Files: internal/config/config.go, internal/provider/anthropic.go 2. Structured forward-error diagnostic logging - When a forward to Anthropic fails, log a single key=value line with request_id, model, stream, body_bytes, has_thinking, anthropic_beta, query, elapsed, ctx_err — alongside the existing human-readable error line for back-compat - Files: internal/handler/handlers.go (logForwardFailure) 3. Full SSE protocol passthrough + Flusher fix - handler/handlers.go: forward all SSE lines verbatim (event:, id:, retry:, : comments, blank-line terminators), not only data:. Previous code produced malformed SSE for strict parsers. - middleware/logging.go: explicit Flush() method on responseWriter. Embedding http.ResponseWriter (interface) does not auto-promote Flush(), so every w.(http.Flusher) check in the streaming handler was returning ok=false and SSE writes buffered in net/http until the body closed. 4. Non-streaming → streaming demotion (feature-flagged) - ANTHROPIC_DEMOTE_NONSTREAMING env (default false) - When enabled and the routed provider is anthropic, force stream=true upstream for clients that asked for stream=false. Receive SSE, accumulate via accumulateSSEToMessage (handles text, tool_use with partial_json reassembly, thinking, signature, citations_delta, usage merge), and synthesize a single non-streaming JSON response. - Eliminates the ResponseHeaderTimeout class of failure entirely. - Body rewrite uses json.Decoder + UseNumber() to preserve integer precision in unknown nested fields (tool inputs from prior turns). - Files: internal/config/config.go, internal/handler/handlers.go, cmd/proxy/main.go, cmd/proxy/main_test.go 5. Live operational state: /livez gauge + graceful drain - New internal/runtime package: atomic in-flight counter + draining flag - New middleware/inflight.go: increments runtime gauge, applied to /v1/* subrouter so Messages, ChatCompletions, and ProxyPassthrough are all counted - /v1/* moved to a gorilla/mux subrouter so the InFlight middleware applies surgically; /health, /livez, /openapi.* remain on parent router (unauthenticated, uncounted) - Health handler returns 503 draining when runtime.IsDraining() is true, so Traefik stops routing to a slot before drain begins - New /livez handler returns {status, in_flight, draining, timestamp} - SIGTERM handler in main.go: SetDraining(true), poll for in_flight==0 with 32-min ceiling and 1s tick (logs every 10s), then srv.Shutdown - Auth bypass list extended with /livez - Files: internal/runtime/runtime.go (new), internal/middleware/inflight.go (new), internal/middleware/auth.go, internal/handler/handlers.go (Health, Livez, runtime import), cmd/proxy/main.go (subrouter, drain loop) 6. OpenAPI spec updates - Document Health 503 response and new DrainingResponse schema - Add /livez path with LivezResponse schema - Files: internal/handler/openapi.go Verified: go build ./... clean, go test ./... all pass, go vet clean. Three rounds of codex peer review across changes 1-5; all feedback addressed (citations_delta, json.Number precision, drain-loop logging via lastLog timestamp, PathPrefix tightened to "/v1/"). --- .dockerignore | 6 + .env.example | 22 +- .gitignore | 7 +- Dockerfile | 152 +- Makefile | 53 +- README.md | 103 +- config.yaml.example | 29 +- docker-entrypoint.dev.sh | 86 + docker-entrypoint.sh | 61 +- proxy/cmd/proxy/main.go | 164 +- proxy/cmd/proxy/main_test.go | 193 + proxy/go.mod | 1 + proxy/go.sum | 2 + proxy/internal/config/config.go | 107 +- proxy/internal/config/config_test.go | 238 +- proxy/internal/handler/handlers.go | 1303 +- .../handler/handlers_conversations_test.go | 287 + .../handler/handlers_dashboard_test.go | 630 + proxy/internal/handler/handlers_proxy_test.go | 302 + .../handler/handlers_settings_test.go | 209 + proxy/internal/handler/openapi.go | 900 ++ proxy/internal/handler/utils.go | 413 +- proxy/internal/middleware/auth.go | 21 +- proxy/internal/middleware/auth_test.go | 19 +- proxy/internal/middleware/dashboard_auth.go | 33 + .../dashboard_auth_protocol_test.go | 41 + .../middleware/dashboard_auth_test.go | 105 + proxy/internal/middleware/inflight.go | 22 + proxy/internal/middleware/logging.go | 186 +- proxy/internal/model/models.go | 160 +- proxy/internal/provider/anthropic.go | 23 +- proxy/internal/provider/openai.go | 522 +- proxy/internal/runtime/runtime.go | 34 + proxy/internal/service/anthropic.go | 122 - proxy/internal/service/conversation.go | 13 + proxy/internal/service/conversation_test.go | 13 +- proxy/internal/service/storage.go | 16 + .../service/storage_aggregation_test.go | 64 + proxy/internal/service/storage_analytics.go | 163 + .../service/storage_analytics_test.go | 83 + .../internal/service/storage_contract_test.go | 217 + proxy/internal/service/storage_decode.go | 46 + proxy/internal/service/storage_decode_test.go | 104 + proxy/internal/service/storage_migrations.go | 25 + proxy/internal/service/storage_payload.go | 203 + .../internal/service/storage_payload_test.go | 163 + proxy/internal/service/storage_postgres.go | 1074 ++ .../service/storage_postgres_contract_test.go | 60 + .../internal/service/storage_query_helpers.go | 38 + proxy/internal/service/storage_sqlite.go | 701 +- .../service/storage_sqlite_contract_test.go | 19 + run.sh | 28 +- shared/frontend/backend.ts | 13 + shared/frontend/formatters.ts | 382 + shared/frontend/types.ts | 307 + shared/server/dashboard_auth.ts | 49 + svelte/package-lock.json | 2851 ++++ svelte/package.json | 32 + svelte/postcss.config.js | 6 + svelte/src/app.css | 333 + svelte/src/app.html | 20 + svelte/src/hooks.server.ts | 16 + svelte/src/lib/api.ts | 225 + svelte/src/lib/auth.server.ts | 32 + svelte/src/lib/backend.server.ts | 10 + svelte/src/lib/chat-formatters.ts | 101 + svelte/src/lib/chat-utils.ts | 242 + svelte/src/lib/components/ChartCanvas.svelte | 84 + svelte/src/lib/components/ChatMessage.svelte | 104 + .../lib/components/ChatOutsideBlock.svelte | 68 + .../lib/components/ChatRequestDetail.svelte | 444 + svelte/src/lib/components/ChatSidebar.svelte | 140 + .../src/lib/components/ChatToolBlock.svelte | 182 + svelte/src/lib/components/CodeDiff.svelte | 65 + svelte/src/lib/components/CodeViewer.svelte | 169 + .../lib/components/ConversationThread.svelte | 138 + svelte/src/lib/components/ImageContent.svelte | 90 + .../src/lib/components/MessageContent.svelte | 226 + svelte/src/lib/components/MessageFlow.svelte | 182 + svelte/src/lib/components/Nav.svelte | 232 + .../components/RequestDetailContent.svelte | 687 + svelte/src/lib/components/RichText.svelte | 105 + .../src/lib/components/RichTextInline.svelte | 79 + svelte/src/lib/components/ThemeToggle.svelte | 20 + svelte/src/lib/components/TodoList.svelte | 92 + svelte/src/lib/components/ToolResult.svelte | 182 + svelte/src/lib/components/ToolUse.svelte | 173 + svelte/src/lib/components/XmlBlock.svelte | 87 + svelte/src/lib/formatters.ts | 1 + svelte/src/lib/models.ts | 29 + svelte/src/lib/pricing.ts | 118 + svelte/src/lib/rich-text.ts | 170 + svelte/src/lib/theme.svelte.ts | 45 + svelte/src/lib/types.ts | 1 + svelte/src/routes/+error.svelte | 19 + svelte/src/routes/+layout.server.ts | 25 + svelte/src/routes/+layout.svelte | 18 + svelte/src/routes/+page.server.ts | 1 + svelte/src/routes/+page.svelte | 318 + svelte/src/routes/analytics/+page.svelte | 980 ++ .../src/routes/api/conversations/+server.ts | 13 + .../routes/api/conversations/[id]/+server.ts | 14 + svelte/src/routes/api/grade-prompt/+server.ts | 19 + svelte/src/routes/api/requests/+server.ts | 24 + .../src/routes/api/requests/[id]/+server.ts | 13 + .../api/requests/latest-date/+server.ts | 13 + .../routes/api/requests/summary/+server.ts | 13 + svelte/src/routes/api/settings/+server.ts | 25 + svelte/src/routes/api/stats/+server.ts | 13 + .../src/routes/api/stats/dashboard/+server.ts | 13 + svelte/src/routes/api/stats/hourly/+server.ts | 13 + svelte/src/routes/api/stats/models/+server.ts | 13 + .../routes/api/stats/organizations/+server.ts | 13 + svelte/src/routes/chat/+page.svelte | 217 + svelte/src/routes/conversations/+page.svelte | 194 + svelte/src/routes/settings/+page.svelte | 274 + {web/public => svelte/static}/favicon.ico | Bin svelte/static/fonts/inter-latin-ext.woff2 | Bin 0 -> 24368 bytes svelte/static/fonts/inter-latin.woff2 | Bin 0 -> 23692 bytes svelte/svelte.config.js | 12 + svelte/tailwind.config.ts | 23 + svelte/tsconfig.json | 14 + svelte/vite.config.ts | 32 + web/.eslintrc.cjs | 84 - web/app/components/CodeDiff.tsx | 103 - web/app/components/CodeViewer.tsx | 236 - web/app/components/ConversationThread.tsx | 203 - web/app/components/ImageContent.tsx | 144 - web/app/components/MessageContent.tsx | 400 - web/app/components/MessageFlow.tsx | 282 - web/app/components/RequestDetailContent.tsx | 1024 -- web/app/components/TodoList.tsx | 190 - web/app/components/ToolResult.tsx | 257 - web/app/components/ToolUse.tsx | 209 - web/app/entry.client.tsx | 18 - web/app/entry.server.tsx | 140 - web/app/root.tsx | 45 - web/app/routes/_index.tsx | 914 -- web/app/routes/api.conversations.tsx | 26 - web/app/routes/api.grade-prompt.tsx | 33 - web/app/routes/api.requests.tsx | 61 - web/app/tailwind.css | 58 - web/app/utils/formatters.ts | 174 - web/app/utils/models.ts | 32 - web/package-lock.json | 13468 ---------------- web/package.json | 44 - web/postcss.config.js | 6 - web/public/logo-dark.png | Bin 80332 -> 0 bytes web/public/logo-light.png | Bin 5906 -> 0 bytes web/tailwind.config.ts | 31 - web/tsconfig.json | 32 - web/vite.config.ts | 32 - 152 files changed, 19227 insertions(+), 19463 deletions(-) create mode 100755 docker-entrypoint.dev.sh mode change 100644 => 100755 docker-entrypoint.sh create mode 100644 proxy/cmd/proxy/main_test.go create mode 100644 proxy/internal/handler/handlers_conversations_test.go create mode 100644 proxy/internal/handler/handlers_dashboard_test.go create mode 100644 proxy/internal/handler/handlers_proxy_test.go create mode 100644 proxy/internal/handler/handlers_settings_test.go create mode 100644 proxy/internal/handler/openapi.go create mode 100644 proxy/internal/middleware/dashboard_auth.go create mode 100644 proxy/internal/middleware/dashboard_auth_protocol_test.go create mode 100644 proxy/internal/middleware/dashboard_auth_test.go create mode 100644 proxy/internal/middleware/inflight.go create mode 100644 proxy/internal/runtime/runtime.go delete mode 100644 proxy/internal/service/anthropic.go create mode 100644 proxy/internal/service/storage_aggregation_test.go create mode 100644 proxy/internal/service/storage_analytics.go create mode 100644 proxy/internal/service/storage_analytics_test.go create mode 100644 proxy/internal/service/storage_contract_test.go create mode 100644 proxy/internal/service/storage_decode.go create mode 100644 proxy/internal/service/storage_decode_test.go create mode 100644 proxy/internal/service/storage_migrations.go create mode 100644 proxy/internal/service/storage_payload.go create mode 100644 proxy/internal/service/storage_payload_test.go create mode 100644 proxy/internal/service/storage_postgres.go create mode 100644 proxy/internal/service/storage_postgres_contract_test.go create mode 100644 proxy/internal/service/storage_query_helpers.go create mode 100644 proxy/internal/service/storage_sqlite_contract_test.go create mode 100644 shared/frontend/backend.ts create mode 100644 shared/frontend/formatters.ts create mode 100644 shared/frontend/types.ts create mode 100644 shared/server/dashboard_auth.ts create mode 100644 svelte/package-lock.json create mode 100644 svelte/package.json create mode 100644 svelte/postcss.config.js create mode 100644 svelte/src/app.css create mode 100644 svelte/src/app.html create mode 100644 svelte/src/hooks.server.ts create mode 100644 svelte/src/lib/api.ts create mode 100644 svelte/src/lib/auth.server.ts create mode 100644 svelte/src/lib/backend.server.ts create mode 100644 svelte/src/lib/chat-formatters.ts create mode 100644 svelte/src/lib/chat-utils.ts create mode 100644 svelte/src/lib/components/ChartCanvas.svelte create mode 100644 svelte/src/lib/components/ChatMessage.svelte create mode 100644 svelte/src/lib/components/ChatOutsideBlock.svelte create mode 100644 svelte/src/lib/components/ChatRequestDetail.svelte create mode 100644 svelte/src/lib/components/ChatSidebar.svelte create mode 100644 svelte/src/lib/components/ChatToolBlock.svelte create mode 100644 svelte/src/lib/components/CodeDiff.svelte create mode 100644 svelte/src/lib/components/CodeViewer.svelte create mode 100644 svelte/src/lib/components/ConversationThread.svelte create mode 100644 svelte/src/lib/components/ImageContent.svelte create mode 100644 svelte/src/lib/components/MessageContent.svelte create mode 100644 svelte/src/lib/components/MessageFlow.svelte create mode 100644 svelte/src/lib/components/Nav.svelte create mode 100644 svelte/src/lib/components/RequestDetailContent.svelte create mode 100644 svelte/src/lib/components/RichText.svelte create mode 100644 svelte/src/lib/components/RichTextInline.svelte create mode 100644 svelte/src/lib/components/ThemeToggle.svelte create mode 100644 svelte/src/lib/components/TodoList.svelte create mode 100644 svelte/src/lib/components/ToolResult.svelte create mode 100644 svelte/src/lib/components/ToolUse.svelte create mode 100644 svelte/src/lib/components/XmlBlock.svelte create mode 100644 svelte/src/lib/formatters.ts create mode 100644 svelte/src/lib/models.ts create mode 100644 svelte/src/lib/pricing.ts create mode 100644 svelte/src/lib/rich-text.ts create mode 100644 svelte/src/lib/theme.svelte.ts create mode 100644 svelte/src/lib/types.ts create mode 100644 svelte/src/routes/+error.svelte create mode 100644 svelte/src/routes/+layout.server.ts create mode 100644 svelte/src/routes/+layout.svelte create mode 100644 svelte/src/routes/+page.server.ts create mode 100644 svelte/src/routes/+page.svelte create mode 100644 svelte/src/routes/analytics/+page.svelte create mode 100644 svelte/src/routes/api/conversations/+server.ts create mode 100644 svelte/src/routes/api/conversations/[id]/+server.ts create mode 100644 svelte/src/routes/api/grade-prompt/+server.ts create mode 100644 svelte/src/routes/api/requests/+server.ts create mode 100644 svelte/src/routes/api/requests/[id]/+server.ts create mode 100644 svelte/src/routes/api/requests/latest-date/+server.ts create mode 100644 svelte/src/routes/api/requests/summary/+server.ts create mode 100644 svelte/src/routes/api/settings/+server.ts create mode 100644 svelte/src/routes/api/stats/+server.ts create mode 100644 svelte/src/routes/api/stats/dashboard/+server.ts create mode 100644 svelte/src/routes/api/stats/hourly/+server.ts create mode 100644 svelte/src/routes/api/stats/models/+server.ts create mode 100644 svelte/src/routes/api/stats/organizations/+server.ts create mode 100644 svelte/src/routes/chat/+page.svelte create mode 100644 svelte/src/routes/conversations/+page.svelte create mode 100644 svelte/src/routes/settings/+page.svelte rename {web/public => svelte/static}/favicon.ico (100%) create mode 100644 svelte/static/fonts/inter-latin-ext.woff2 create mode 100644 svelte/static/fonts/inter-latin.woff2 create mode 100644 svelte/svelte.config.js create mode 100644 svelte/tailwind.config.ts create mode 100644 svelte/tsconfig.json create mode 100644 svelte/vite.config.ts delete mode 100644 web/.eslintrc.cjs delete mode 100644 web/app/components/CodeDiff.tsx delete mode 100644 web/app/components/CodeViewer.tsx delete mode 100644 web/app/components/ConversationThread.tsx delete mode 100644 web/app/components/ImageContent.tsx delete mode 100644 web/app/components/MessageContent.tsx delete mode 100644 web/app/components/MessageFlow.tsx delete mode 100644 web/app/components/RequestDetailContent.tsx delete mode 100644 web/app/components/TodoList.tsx delete mode 100644 web/app/components/ToolResult.tsx delete mode 100644 web/app/components/ToolUse.tsx delete mode 100644 web/app/entry.client.tsx delete mode 100644 web/app/entry.server.tsx delete mode 100644 web/app/root.tsx delete mode 100644 web/app/routes/_index.tsx delete mode 100644 web/app/routes/api.conversations.tsx delete mode 100644 web/app/routes/api.grade-prompt.tsx delete mode 100644 web/app/routes/api.requests.tsx delete mode 100644 web/app/tailwind.css delete mode 100644 web/app/utils/formatters.ts delete mode 100644 web/app/utils/models.ts delete mode 100644 web/package-lock.json delete mode 100644 web/package.json delete mode 100644 web/postcss.config.js delete mode 100644 web/public/logo-dark.png delete mode 100644 web/public/logo-light.png delete mode 100644 web/tailwind.config.ts delete mode 100644 web/tsconfig.json delete mode 100644 web/vite.config.ts 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 0000000000000000000000000000000000000000..03aaea1ce7dcce7ccef4cf7fc116a904d50aa204 GIT binary patch literal 24368 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 aUbWq0We;bU+VQC=KKz{j$8G)n1^@ua=Md%q literal 0 HcmV?d00001 diff --git a/svelte/static/fonts/inter-latin.woff2 b/svelte/static/fonts/inter-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..33002f12853a3d37d7b7eba39623fae3bca94a3b GIT binary patch literal 23692 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 { + 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 b24c7aee3a86dad7eb334ba6fba94e5b18b84215..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80332 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 BAPKNXC&B9%2=3YBbo!Y+L4D-GS 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�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 F{{et{IHv#r 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, - }, - }, - }, -});