diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4c9d3b..8725c28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,3 +70,27 @@ jobs: - name: Check for TypeScript errors run: npm run type-check + + ci-status: + runs-on: ubuntu-latest + continue-on-error: false + name: CI Status Check + needs: [build-and-test, lint-check] + if: always() + permissions: + statuses: write + steps: + - name: Failed + id: failed + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: | + curl -X POST -H "Content-Type: application/json" -H "Authorization: token ${{ github.token }}" \ + -d '{ "state" : "failure" , "context" : "github/ci-status-check" , "description" : "CI checks failed", "target_url" : "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" }' \ + https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} + exit 1 + - name: Success + if: steps.failed.conclusion == 'skipped' + run: | + curl -X POST -H "Content-Type: application/json" -H "Authorization: token ${{ github.token }}" \ + -d '{ "state" : "success" , "context" : "github/ci-status-check" , "description" : "CI checks passed", "target_url" : "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" }' \ + https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1447ed7..c8ae0d7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -10,9 +10,9 @@ on: workflow_dispatch: env: - REGISTRY: ghcr.io - IMAGE_NAME_MCP: ${{ github.repository }} - IMAGE_NAME_CANVAS: ${{ github.repository }}-canvas + REGISTRY: docker.io + IMAGE_NAME_MCP: sanjibdevnath/mcp-excalidraw-local + IMAGE_NAME_CANVAS: sanjibdevnath/mcp-excalidraw-local-canvas jobs: build-and-push-mcp: @@ -20,7 +20,6 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - packages: write steps: - name: Checkout code @@ -29,12 +28,12 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to GitHub Container Registry + - name: Log in to Docker Hub + if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata for MCP Server id: meta @@ -55,19 +54,18 @@ jobs: with: context: . file: ./Dockerfile - push: true + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - platforms: linux/amd64,linux/arm64 + platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} build-and-push-canvas: name: Build and Push Canvas Server Image runs-on: ubuntu-latest permissions: contents: read - packages: write steps: - name: Checkout code @@ -76,12 +74,12 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to GitHub Container Registry + - name: Log in to Docker Hub + if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata for Canvas Server id: meta @@ -102,39 +100,30 @@ jobs: with: context: . file: ./Dockerfile.canvas - push: true + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - platforms: linux/amd64,linux/arm64 + platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} test-docker-images: name: Test Docker Images needs: [build-and-push-mcp, build-and-push-canvas] + if: github.event_name != 'pull_request' runs-on: ubuntu-latest steps: - - name: Log in to GitHub Container Registry + - name: Log in to Docker Hub uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Determine image tag - id: tag - run: | - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "value=pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT - else - echo "value=${{ github.ref_name }}" >> $GITHUB_OUTPUT - fi + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Test Canvas Server image run: | - docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_CANVAS }}:${{ steps.tag.outputs.value }} - docker run -d -p 3000:3000 --name test-canvas ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_CANVAS }}:${{ steps.tag.outputs.value }} + docker pull ${{ env.IMAGE_NAME_CANVAS }}:latest + docker run -d -p 3000:3000 --name test-canvas ${{ env.IMAGE_NAME_CANVAS }}:latest sleep 10 curl -f http://localhost:3000/health || exit 1 docker logs test-canvas @@ -142,5 +131,29 @@ jobs: - name: Test MCP Server image run: | - docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_MCP }}:${{ steps.tag.outputs.value }} + docker pull ${{ env.IMAGE_NAME_MCP }}:latest echo "MCP Server image pulled successfully" + + docker-status: + runs-on: ubuntu-latest + continue-on-error: false + name: Docker Build Status Check + needs: [build-and-push-mcp, build-and-push-canvas] + if: always() + permissions: + statuses: write + steps: + - name: Failed + id: failed + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: | + curl -X POST -H "Content-Type: application/json" -H "Authorization: token ${{ github.token }}" \ + -d '{ "state" : "failure" , "context" : "github/docker-build-check" , "description" : "Docker build failed", "target_url" : "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" }' \ + https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} + exit 1 + - name: Success + if: steps.failed.conclusion == 'skipped' + run: | + curl -X POST -H "Content-Type: application/json" -H "Authorization: token ${{ github.token }}" \ + -d '{ "state" : "success" , "context" : "github/docker-build-check" , "description" : "Docker build passed", "target_url" : "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" }' \ + https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 18237f4..718aa7e 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -53,7 +53,7 @@ jobs: - name: Check if version exists on NPM id: check-version run: | - if npm view mcp-excalidraw-server@${{ steps.package-version.outputs.version }} version 2>/dev/null; then + if npm view @sanjibdevnath/mcp-excalidraw-local@${{ steps.package-version.outputs.version }} version 2>/dev/null; then echo "exists=true" >> $GITHUB_OUTPUT echo "Version ${{ steps.package-version.outputs.version }} already exists on NPM" else @@ -82,14 +82,14 @@ jobs: - name: Create GitHub Release Assets if: github.event_name == 'release' run: | - tar -czf mcp-excalidraw-server-${{ steps.package-version.outputs.version }}.tar.gz dist/ + tar -czf mcp-excalidraw-local-${{ steps.package-version.outputs.version }}.tar.gz dist/ - name: Upload Release Assets if: github.event_name == 'release' uses: softprops/action-gh-release@v1 with: files: | - mcp-excalidraw-server-${{ steps.package-version.outputs.version }}.tar.gz + mcp-excalidraw-local-${{ steps.package-version.outputs.version }}.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -103,4 +103,4 @@ jobs: - name: Success notification run: | echo "✅ Package successfully published to NPM!" - echo "View at: https://www.npmjs.com/package/mcp-excalidraw-server" + echo "View at: https://www.npmjs.com/package/@sanjibdevnath/mcp-excalidraw-local" diff --git a/.gitignore b/.gitignore index f853524..e99a849 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ public/dist/ # Development artifacts *.excalidraw -docs/ \ No newline at end of file +docs/* +!docs/screenshots/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index aef20cd..2d92c4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,58 +1,47 @@ # Dockerfile for MCP Excalidraw Server -# This builds the MCP server only (core product for CI/CD and GHCR) -# The canvas server is optional and runs separately +# Builds the MCP server with SQLite persistence -# Stage 1: Build backend (TypeScript compilation) +# Stage 1: Build backend (TypeScript compilation + native modules) FROM node:18-slim AS builder +RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* + WORKDIR /app -# Copy package files COPY package*.json ./ +RUN --mount=type=cache,target=/root/.npm npm ci -# Install all dependencies (including TypeScript compiler) -RUN npm ci && npm cache clean --force - -# Copy backend source COPY src ./src COPY tsconfig.json ./ - -# Compile TypeScript RUN npm run build:server # Stage 2: Production MCP Server FROM node:18-slim AS production -# Create non-root user for security +RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* + RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 --gid 1001 nodejs WORKDIR /app -# Copy package files COPY package*.json ./ +RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev -# Install only production dependencies -RUN npm ci --only=production && npm cache clean --force +# Remove build tools after native modules are compiled +RUN apt-get purge -y python3 make g++ && apt-get autoremove -y -# Copy compiled backend (MCP server only) COPY --from=builder /app/dist ./dist -# Set ownership to nodejs user RUN chown -R nodejs:nodejs /app - -# Switch to non-root user USER nodejs -# Set environment variables with defaults ENV NODE_ENV=production ENV EXPRESS_SERVER_URL=http://localhost:3000 ENV ENABLE_CANVAS_SYNC=true -# Run MCP server (stdin/stdout protocol) CMD ["node", "dist/index.js"] -# Labels for metadata -LABEL org.opencontainers.image.source="https://github.com/yctimlin/mcp_excalidraw" -LABEL org.opencontainers.image.description="MCP Excalidraw Server - Model Context Protocol for AI agents" +LABEL org.opencontainers.image.source="https://github.com/sanjibdevnathlabs/mcp-excalidraw-local" +LABEL org.opencontainers.image.description="MCP Excalidraw Server - Model Context Protocol for AI agents (with SQLite persistence & multi-tenancy)" LABEL org.opencontainers.image.licenses="MIT" diff --git a/Dockerfile.canvas b/Dockerfile.canvas index 9a42461..65f74ca 100644 --- a/Dockerfile.canvas +++ b/Dockerfile.canvas @@ -1,82 +1,62 @@ # Dockerfile for Canvas Server (Optional) -# This is for users who want to run the visual canvas UI -# The canvas server provides the web interface and REST API +# Provides the web interface, REST API, and SQLite persistence # Stage 1: Build frontend FROM node:18-slim AS frontend-builder WORKDIR /app -# Copy package files COPY package*.json ./ +RUN --mount=type=cache,target=/root/.npm npm ci --ignore-scripts -# Install all dependencies (including dev dependencies for build) -RUN npm ci && npm cache clean --force - -# Copy frontend source COPY frontend ./frontend COPY vite.config.js ./ - -# Build frontend RUN npm run build:frontend -# Stage 2: Build backend (TypeScript compilation) +# Stage 2: Build backend (TypeScript compilation + native modules) FROM node:18-slim AS backend-builder +RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* + WORKDIR /app -# Copy package files COPY package*.json ./ +RUN --mount=type=cache,target=/root/.npm npm ci -# Install all dependencies (including TypeScript compiler) -RUN npm ci && npm cache clean --force - -# Copy backend source COPY src ./src COPY tsconfig.json ./ - -# Compile TypeScript RUN npm run build:server # Stage 3: Production Canvas Server FROM node:18-slim AS production -# Create non-root user for security +RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* + RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 --gid 1001 nodejs WORKDIR /app -# Copy package files COPY package*.json ./ +RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev -# Install only production dependencies -RUN npm ci --only=production && npm cache clean --force +# Remove build tools after native modules are compiled +RUN apt-get purge -y python3 make g++ && apt-get autoremove -y -# Copy compiled backend from builder stage COPY --from=backend-builder /app/dist ./dist - -# Copy built frontend from frontend-builder stage COPY --from=frontend-builder /app/dist/frontend ./dist/frontend -# Set ownership to nodejs user RUN chown -R nodejs:nodejs /app - -# Switch to non-root user USER nodejs -# Set environment variables with defaults ENV NODE_ENV=production ENV PORT=3000 ENV HOST=0.0.0.0 -# Expose port EXPOSE 3000 -# Run canvas server (web UI + REST API) CMD ["node", "dist/server.js"] -# Labels for metadata -LABEL org.opencontainers.image.source="https://github.com/yctimlin/mcp_excalidraw" -LABEL org.opencontainers.image.description="MCP Excalidraw Canvas Server - Web UI and REST API" +LABEL org.opencontainers.image.source="https://github.com/sanjibdevnathlabs/mcp-excalidraw-local" +LABEL org.opencontainers.image.description="MCP Excalidraw Canvas Server - Web UI and REST API (with SQLite persistence & multi-tenancy)" LABEL org.opencontainers.image.licenses="MIT" diff --git a/README.md b/README.md index bff6dfe..bb6fc58 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,69 @@ -# Excalidraw MCP Server & Agent Skill +# MCP Excalidraw Local -[![CI](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/ci.yml/badge.svg)](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/ci.yml) -[![Docker Build & Push](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/docker.yml/badge.svg)](https://github.com/yctimlin/mcp_excalidraw/actions/workflows/docker.yml) -[![NPM Version](https://img.shields.io/npm/v/mcp-excalidraw-server)](https://www.npmjs.com/package/mcp-excalidraw-server) +[![CI](https://github.com/sanjibdevnathlabs/mcp-excalidraw-local/actions/workflows/ci.yml/badge.svg)](https://github.com/sanjibdevnathlabs/mcp-excalidraw-local/actions/workflows/ci.yml) +[![Docker Build & Push](https://github.com/sanjibdevnathlabs/mcp-excalidraw-local/actions/workflows/docker.yml/badge.svg)](https://github.com/sanjibdevnathlabs/mcp-excalidraw-local/actions/workflows/docker.yml) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -Run a live Excalidraw canvas and control it from AI agents. This repo provides: +A fully local, self-hosted Excalidraw MCP server with **SQLite persistence**, **multi-tenancy**, and **auto-sync** — designed to run entirely on your machine without depending on `excalidraw.com`. -- **MCP Server**: Connect via Model Context Protocol (Claude Desktop, Cursor, Codex CLI, etc.) -- **Agent Skill**: Portable skill for Claude Code, Codex CLI, and other skill-enabled agents +Run a live Excalidraw canvas and control it from any AI agent. This repo provides: -Keywords: Excalidraw agent skill, Excalidraw MCP server, AI diagramming, Claude Code skill, Codex CLI skill, Claude Desktop MCP, Cursor MCP, Mermaid to Excalidraw. +- **MCP Server**: 32 tools over stdio — works with any MCP-compatible client +- **Agent Skill**: Portable skill with workflow playbooks, cheatsheets, and helper scripts +- **Live Canvas**: Real-time Excalidraw UI synced via WebSocket +- **SQLite Persistence**: Elements survive restarts, with versioning and search +- **Multi-Tenancy**: Isolated canvases per workspace, auto-detected -## Demo +> **Fork notice:** This project is forked from [yctimlin/mcp_excalidraw](https://github.com/yctimlin/mcp_excalidraw) and extends it with persistence, multi-workspace support, and numerous UX improvements. Full credit to the original author for the excellent foundation. See [What Changed From Upstream](#what-changed-from-upstream) for details. -![MCP Excalidraw Demo](demo.gif) +Keywords: Excalidraw MCP server, AI diagramming, local Excalidraw, self-hosted, SQLite persistence, multi-tenant, Mermaid to Excalidraw. -*AI agent creates a complete architecture diagram from a single prompt (4x speed). [Watch full video on YouTube](https://youtu.be/ufW78Amq5qA)* +## Screenshots + +### Canvas UI + +The live Excalidraw canvas with toolbar, connection status, sync controls, and workspace badge: + +![Canvas UI](docs/screenshots/canvas-ui.png) + +### Workspace Switcher + +Click the workspace badge to switch between isolated canvases — each workspace has its own set of diagrams: + +![Workspace Switcher](docs/screenshots/workspace-switcher.png) + +> For a demo of the upstream project (before persistence/multi-tenancy), see the [original video by @yctimlin](https://youtu.be/ufW78Amq5qA). ## Table of Contents -- [Demo](#demo) +- [Screenshots](#screenshots) - [What It Is](#what-it-is) - [How We Differ from the Official Excalidraw MCP](#how-we-differ-from-the-official-excalidraw-mcp) +- [What Changed From Upstream](#what-changed-from-upstream) - [What's New](#whats-new) -- [Quick Start (Local)](#quick-start-local) +- [Architecture](#architecture) +- [Quick Start](#quick-start) - [Quick Start (Docker)](#quick-start-docker) -- [Configure MCP Clients](#configure-mcp-clients) - - [Claude Desktop](#claude-desktop) - - [Claude Code](#claude-code) - - [Cursor](#cursor) - - [Codex CLI](#codex-cli) - - [OpenCode](#opencode) - - [Antigravity (Google)](#antigravity-google) +- [Configuration](#configuration) +- [Environment Variables](#environment-variables) +- [Multi-Tenancy (Workspaces)](#multi-tenancy-workspaces) - [Agent Skill (Optional)](#agent-skill-optional) -- [MCP Tools (26 Total)](#mcp-tools-26-total) +- [MCP Tools (32 Total)](#mcp-tools-32-total) - [Testing](#testing) - [Troubleshooting](#troubleshooting) - [Known Issues / TODO](#known-issues--todo) - [Development](#development) +- [Credits](#credits) ## What It Is -This repo contains two separate processes: +This MCP server gives AI agents a full canvas toolkit to build, inspect, and iteratively refine Excalidraw diagrams — including the ability to see what they drew. -- Canvas server: web UI + REST API + WebSocket updates (default `http://localhost:3000`) -- MCP server: exposes MCP tools over stdio; syncs to the canvas via `EXPRESS_SERVER_URL` +The repo contains a single Node.js process that runs: + +- **MCP server** (stdio): 32 tools for element CRUD, layout, scene awareness, file I/O, snapshots, search, multi-tenancy, and more +- **Canvas server** (embedded): web UI + REST API + WebSocket updates at `http://localhost:` +- **SQLite database**: persistent storage at `~/.excalidraw-mcp/excalidraw.db` ## How We Differ from the Official Excalidraw MCP @@ -53,8 +71,10 @@ Excalidraw now has an [official MCP](https://github.com/excalidraw/excalidraw-mc | | Official Excalidraw MCP | This Project | |---|---|---| -| **Approach** | Prompt in, diagram out (one-shot) | Programmatic element-level control (26 tools) | +| **Approach** | Prompt in, diagram out (one-shot) | Programmatic element-level control (32 tools) | | **State** | Stateless — each call is independent | Persistent live canvas with real-time sync | +| **Storage** | None | SQLite with WAL mode, versioning, element history | +| **Multi-tenancy** | No | Workspace-based isolation, auto-detected | | **Element CRUD** | No | Full create / read / update / delete per element | | **AI sees the canvas** | No | `describe_scene` (structured text) + `get_canvas_screenshot` (image) | | **Iterative refinement** | No — regenerate the whole diagram | Draw → look → adjust → look again, element by element | @@ -62,7 +82,7 @@ Excalidraw now has an [official MCP](https://github.com/excalidraw/excalidraw-mc | **File I/O** | No | `export_scene` / `import_scene` (.excalidraw JSON) | | **Snapshot & rollback** | No | `snapshot_scene` / `restore_snapshot` | | **Mermaid conversion** | No | `create_from_mermaid` | -| **Shareable URLs** | Yes | Yes — `export_to_excalidraw_url` | +| **Search** | No | `search_elements` — full-text search across labels | | **Design guide** | `read_me` cheat sheet | `read_diagram_guide` (colors, sizing, layout, anti-patterns) | | **Viewport control** | Camera animations | `set_viewport` (zoom-to-fit, center on element, manual zoom) | | **Live canvas UI** | Rendered inline in chat | Standalone Excalidraw app synced via WebSocket | @@ -71,338 +91,261 @@ Excalidraw now has an [official MCP](https://github.com/excalidraw/excalidraw-mc **TL;DR** — The official MCP generates diagrams. We give AI agents a full canvas toolkit to build, inspect, and iteratively refine diagrams — including the ability to see what they drew. +## What Changed From Upstream + +This fork extends [yctimlin/mcp_excalidraw](https://github.com/yctimlin/mcp_excalidraw) with the following enhancements: + +| Area | Upstream | This Fork | +|---|---|---| +| **Storage** | In-memory (lost on restart) | SQLite with WAL mode, versioning, element history | +| **Multi-tenancy** | None | Workspace-based tenant isolation (auto-detected via `server.listRoots()`) | +| **Canvas lifecycle** | Separate process (2 terminals) | Embedded in MCP process (single `node dist/index.js`) | +| **Auto-sync** | Manual "Sync to Backend" button | Debounced auto-sync (3s idle) with manual override | +| **Canvas port** | Hardcoded 3000 | Configurable via `CANVAS_PORT` env var | +| **MCP tools** | 26 | 32 (added search, history, tenants, projects) | +| **Workspace switcher** | None | Dropdown with search in canvas UI | +| **Sync normalization** | Bound text breaks on reload | Elements normalized to MCP format before storage | +| **Projects** | None | Multiple projects per tenant | +| **Element history** | None | Full version history per element | +| **Search** | None | Full-text search across elements | + +### New MCP Tools (6 added) + +| Tool | Description | +|---|---| +| `search_elements` | Full-text search across element labels and text | +| `element_history` | View version history for any element | +| `list_projects` | List projects within the active tenant | +| `switch_project` | Switch between projects | +| `list_tenants` | List all workspace tenants | +| `switch_tenant` | Switch the active workspace tenant | + ## What's New -### v2.0 — Canvas Toolkit +### v3.0 — This Fork (Persistence & Multi-Tenancy) + +- **SQLite persistence**: Elements, projects, tenants, snapshots, and element versions stored in `~/.excalidraw-mcp/excalidraw.db` with WAL mode and `busy_timeout` for multi-process safety +- **Multi-tenancy**: Each workspace gets an isolated canvas. Tenant auto-detected from workspace path via `server.listRoots()`. UI dropdown with search for switching workspaces +- **Embedded canvas**: Canvas server runs inside the MCP process — single `node dist/index.js` starts everything, stops together +- **Auto-sync with debounce**: Canvas changes are automatically persisted after 3s of inactivity. Manual sync button as fallback. Toggle auto-sync on/off +- **Configurable port**: `CANVAS_PORT` env var (default `3000`) +- **Sync normalization**: Excalidraw's internal bound-text representation is normalized to MCP format before storage, preventing text overflow/detachment on reload +- **6 new MCP tools**: `search_elements`, `element_history`, `list_projects`, `switch_project`, `list_tenants`, `switch_tenant` +- **Updated agent skill**: Comprehensive workflow playbook with iterative write-check-review cycle, sizing rules, anti-patterns, and quality checklist +- **Workspace switcher UI**: Click "Workspace: ..." badge to search and switch between workspaces + +### v2.0 — Canvas Toolkit (upstream) - 13 new MCP tools (26 total): `get_element`, `clear_canvas`, `export_scene`, `import_scene`, `export_to_image`, `duplicate_elements`, `snapshot_scene`, `restore_snapshot`, `describe_scene`, `get_canvas_screenshot`, `read_diagram_guide`, `export_to_excalidraw_url`, `set_viewport` - **Closed feedback loop**: AI can now inspect the canvas (`describe_scene`) and see it (`get_canvas_screenshot` returns an image) — enabling iterative refinement -- **Design guide**: `read_diagram_guide` returns best-practice color palettes, sizing rules, layout patterns, and anti-patterns — dramatically improves AI-generated diagram quality -- **Shareable URLs**: `export_to_excalidraw_url` encrypts and uploads the scene to excalidraw.com, returns a shareable link anyone can open -- **Viewport control**: `set_viewport` with `scrollToContent`, `scrollToElementId`, or manual zoom/offset — agents can auto-fit diagrams after creation +- **Design guide**: `read_diagram_guide` returns best-practice color palettes, sizing rules, layout patterns, and anti-patterns +- **Viewport control**: `set_viewport` with `scrollToContent`, `scrollToElementId`, or manual zoom/offset - **File I/O**: export/import full `.excalidraw` JSON files - **Snapshots**: save and restore named canvas states -- **Skill fallback**: Agent skill auto-detects MCP vs REST API mode, gracefully falls back to HTTP endpoints when MCP server isn't configured -- Fixed all previously known issues: `align_elements` / `distribute_elements` fully implemented, points type normalization, removed invalid `label` type, removed HTTP transport dead code, `ungroup_elements` now errors on failure +- **Skill fallback**: Agent skill auto-detects MCP vs REST API mode +- Fixed all previously known issues: `align_elements` / `distribute_elements` fully implemented, points type normalization, removed invalid `label` type, `ungroup_elements` now errors on failure -### v1.x +### v1.x (upstream) - Agent skill: `skills/excalidraw-skill/` (portable instructions + helper scripts for export/import and repeatable CRUD) -- Better testing loop: MCP Inspector CLI examples + browser screenshot checks (`agent-browser`) -- Bugfixes: batch create now preserves element ids (fixes update/delete after batch); frontend entrypoint fixed (`main.tsx`) +- Better testing loop: MCP Inspector CLI examples + browser screenshot checks +- Bugfixes: batch create now preserves element ids (fixes update/delete after batch); frontend entrypoint fixed -## Quick Start (Local) +## Architecture -Prereqs: Node >= 18, npm - -```bash -npm ci -npm run build +``` +┌─────────────────────────────────────────────────┐ +│ MCP Process (single node process) │ +│ │ +│ ┌──────────────┐ ┌──────────────────────┐ │ +│ │ MCP Server │────▶│ Canvas Server │ │ +│ │ (stdio) │ │ (Express + WS) │ │ +│ │ 32 tools │ │ http://localhost:PORT │ │ +│ └──────────────┘ └──────────┬───────────┘ │ +│ │ │ +│ ┌──────────▼───────────┐ │ +│ │ SQLite Database │ │ +│ │ ~/.excalidraw-mcp/ │ │ +│ │ excalidraw.db │ │ +│ └──────────────────────┘ │ +└─────────────────────────────────────────────────┘ + ▲ ▲ + │ stdio │ HTTP/WS + ┌────┴─────┐ ┌───────┴──────┐ + │ Any MCP │ │ Browser │ + │ Client │ │ :3000 │ + │ │ │ (Excalidraw) │ + └──────────┘ └──────────────┘ ``` -Terminal 1: start the canvas +- **Single process**: The MCP server embeds the canvas server. Starting the MCP starts both; stopping it stops both. +- **SQLite**: Stored at `~/.excalidraw-mcp/excalidraw.db` by default. WAL mode + `busy_timeout` for multi-process safety. +- **Multi-tenancy**: Each workspace gets an isolated tenant (SHA-256 hash of workspace path). The UI shows a workspace switcher dropdown with search. + +## Quick Start + +### Option A: NPM (recommended) + ```bash -HOST=0.0.0.0 PORT=3000 npm run canvas +npx @sanjibdevnath/mcp-excalidraw-local ``` -Open `http://localhost:3000`. +Or install globally: -Terminal 2: run the MCP server (stdio) ```bash -EXPRESS_SERVER_URL=http://localhost:3000 node dist/index.js +npm install -g @sanjibdevnath/mcp-excalidraw-local +mcp-excalidraw-local ``` +### Option B: From source + +**Prerequisites:** Node >= 18, npm or pnpm + +```bash +git clone https://github.com/sanjibdevnathlabs/mcp-excalidraw-local.git +cd mcp-excalidraw-local + +# Install dependencies (pnpm or npm) +pnpm install +pnpm rebuild better-sqlite3 esbuild + +# Build frontend + server +pnpm run build +``` + +The MCP server is typically started by your MCP client — see [Configuration](#configuration). To run manually: + +```bash +node dist/index.js +``` + +This starts the MCP server (stdio) **and** the canvas server. Open `http://localhost:3000` in your browser. + ## Quick Start (Docker) Canvas server: ```bash -docker run -d -p 3000:3000 --name mcp-excalidraw-canvas ghcr.io/yctimlin/mcp_excalidraw-canvas:latest +docker run -d -p 3000:3000 --name mcp-excalidraw-canvas sanjibdevnath/mcp-excalidraw-local-canvas:latest ``` -MCP server (stdio) is typically launched by your MCP client (Claude Desktop/Cursor/etc.). If you want a local container for it, use the image `ghcr.io/yctimlin/mcp_excalidraw:latest` and set `EXPRESS_SERVER_URL` to point at the canvas. +MCP server (stdio) is typically launched by your MCP client. If you want a local container, use `sanjibdevnath/mcp-excalidraw-local:latest`. -## Configure MCP Clients +## Configuration -The MCP server runs over stdio and can be configured with any MCP-compatible client. Below are configurations for both **local** (requires cloning and building) and **Docker** (pull-and-run) setups. +This is a standard MCP server communicating over **stdio**. It works with any MCP-compatible client (Cursor, Claude Desktop, Claude Code, Codex CLI, OpenCode, Gemini, or any other agent that supports the Model Context Protocol). -### Environment Variables +### JSON config (most clients) + +Add this to your client's MCP configuration file: + +```json +{ + "mcpServers": { + "excalidraw-canvas": { + "command": "node", + "args": ["/absolute/path/to/mcp-excalidraw-local/dist/index.js"], + "env": { + "CANVAS_PORT": "3000" + } + } + } +} +``` + +Replace `/absolute/path/to/mcp-excalidraw-local` with the actual path where you cloned and built the repo. + +### CLI-based registration + +```bash +# Example for Claude Code +claude mcp add excalidraw-canvas --scope user \ + -e CANVAS_PORT=3000 \ + -- node /absolute/path/to/mcp-excalidraw-local/dist/index.js +``` + +### Docker + +```json +{ + "mcpServers": { + "excalidraw-canvas": { + "command": "docker", + "args": [ + "run", "-i", "--rm", + "-e", "CANVAS_PORT=3000", + "sanjibdevnath/mcp-excalidraw-local:latest" + ] + } + } +} +``` + +> **Note:** For Docker on Linux, you may need `--add-host=host.docker.internal:host-gateway`. + +### Key points + +- **Single process** — The canvas server is embedded. No separate terminal or process needed. +- **Browser required for screenshots** — `export_to_image` and `get_canvas_screenshot` rely on the frontend. Open `http://localhost:3000` in a browser. + +## Environment Variables | Variable | Description | Default | |----------|-------------|---------| -| `EXPRESS_SERVER_URL` | URL of the canvas server | `http://localhost:3000` | +| `CANVAS_PORT` | Port for the embedded canvas server | `3000` | +| `EXCALIDRAW_DB_PATH` | Path to the SQLite database file | `~/.excalidraw-mcp/excalidraw.db` | +| `EXCALIDRAW_EXPORT_DIR` | Allowed directory for file exports | `process.cwd()` | +| `EXPRESS_SERVER_URL` | Canvas server URL (only if running canvas separately) | `http://localhost:3000` | | `ENABLE_CANVAS_SYNC` | Enable real-time canvas sync | `true` | ---- +## Multi-Tenancy (Workspaces) -### Claude Desktop +Each workspace (codebase) gets an isolated canvas. The tenant is identified by a SHA-256 hash of the workspace path. -Config location: -- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` -- Windows: `%APPDATA%\Claude\claude_desktop_config.json` -- Linux: `~/.config/Claude/claude_desktop_config.json` +### How it works -**Local (node)** -```json -{ - "mcpServers": { - "excalidraw": { - "command": "node", - "args": ["/absolute/path/to/mcp_excalidraw/dist/index.js"], - "env": { - "EXPRESS_SERVER_URL": "http://localhost:3000", - "ENABLE_CANVAS_SYNC": "true" - } - } - } -} -``` +1. **Auto-detection**: When the MCP starts, it calls `server.listRoots()` to get the actual workspace path from the MCP client. This is hashed to create a unique tenant ID. +2. **Per-request scoping**: Every HTTP request includes an `X-Tenant-Id` header. The canvas server uses this to scope all CRUD operations to the correct tenant. +3. **UI switcher**: The canvas UI shows a "Workspace: <name>" badge. Click it to open a dropdown with all known workspaces, complete with search. +4. **Multi-instance safe**: SQLite WAL mode with `busy_timeout = 5000ms` handles concurrent access from multiple client instances. -**Docker** -```json -{ - "mcpServers": { - "excalidraw": { - "command": "docker", - "args": [ - "run", "-i", "--rm", - "-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000", - "-e", "ENABLE_CANVAS_SYNC=true", - "ghcr.io/yctimlin/mcp_excalidraw:latest" - ] - } - } -} -``` +### Projects within a tenant ---- - -### Claude Code - -Use the `claude mcp add` command to register the MCP server. - -**Local (node)** - User-level (available across all projects): -```bash -claude mcp add excalidraw --scope user \ - -e EXPRESS_SERVER_URL=http://localhost:3000 \ - -e ENABLE_CANVAS_SYNC=true \ - -- node /absolute/path/to/mcp_excalidraw/dist/index.js -``` - -**Local (node)** - Project-level (shared via `.mcp.json`): -```bash -claude mcp add excalidraw --scope project \ - -e EXPRESS_SERVER_URL=http://localhost:3000 \ - -e ENABLE_CANVAS_SYNC=true \ - -- node /absolute/path/to/mcp_excalidraw/dist/index.js -``` - -**Docker** -```bash -claude mcp add excalidraw --scope user \ - -- docker run -i --rm \ - -e EXPRESS_SERVER_URL=http://host.docker.internal:3000 \ - -e ENABLE_CANVAS_SYNC=true \ - ghcr.io/yctimlin/mcp_excalidraw:latest -``` - -**Manage servers:** -```bash -claude mcp list # List configured servers -claude mcp remove excalidraw # Remove a server -``` - ---- - -### Cursor - -Config location: `.cursor/mcp.json` in your project root (or `~/.cursor/mcp.json` for global config) - -**Local (node)** -```json -{ - "mcpServers": { - "excalidraw": { - "command": "node", - "args": ["/absolute/path/to/mcp_excalidraw/dist/index.js"], - "env": { - "EXPRESS_SERVER_URL": "http://localhost:3000", - "ENABLE_CANVAS_SYNC": "true" - } - } - } -} -``` - -**Docker** -```json -{ - "mcpServers": { - "excalidraw": { - "command": "docker", - "args": [ - "run", "-i", "--rm", - "-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000", - "-e", "ENABLE_CANVAS_SYNC=true", - "ghcr.io/yctimlin/mcp_excalidraw:latest" - ] - } - } -} -``` - ---- - -### Codex CLI - -Use the `codex mcp add` command to register the MCP server. - -**Local (node)** -```bash -codex mcp add excalidraw \ - --env EXPRESS_SERVER_URL=http://localhost:3000 \ - --env ENABLE_CANVAS_SYNC=true \ - -- node /absolute/path/to/mcp_excalidraw/dist/index.js -``` - -**Docker** -```bash -codex mcp add excalidraw \ - -- docker run -i --rm \ - -e EXPRESS_SERVER_URL=http://host.docker.internal:3000 \ - -e ENABLE_CANVAS_SYNC=true \ - ghcr.io/yctimlin/mcp_excalidraw:latest -``` - -**Manage servers:** -```bash -codex mcp list # List configured servers -codex mcp remove excalidraw # Remove a server -``` - ---- - -### OpenCode - -Config location: `~/.config/opencode/opencode.json` or project-level `opencode.json` - -**Local (node)** -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "excalidraw": { - "type": "local", - "command": ["node", "/absolute/path/to/mcp_excalidraw/dist/index.js"], - "enabled": true, - "environment": { - "EXPRESS_SERVER_URL": "http://localhost:3000", - "ENABLE_CANVAS_SYNC": "true" - } - } - } -} -``` - -**Docker** -```json -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "excalidraw": { - "type": "local", - "command": ["docker", "run", "-i", "--rm", "-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000", "-e", "ENABLE_CANVAS_SYNC=true", "ghcr.io/yctimlin/mcp_excalidraw:latest"], - "enabled": true - } - } -} -``` - ---- - -### Antigravity (Google) - -Config location: `~/.gemini/antigravity/mcp_config.json` - -**Local (node)** -```json -{ - "mcpServers": { - "excalidraw": { - "command": "node", - "args": ["/absolute/path/to/mcp_excalidraw/dist/index.js"], - "env": { - "EXPRESS_SERVER_URL": "http://localhost:3000", - "ENABLE_CANVAS_SYNC": "true" - } - } - } -} -``` - -**Docker** -```json -{ - "mcpServers": { - "excalidraw": { - "command": "docker", - "args": [ - "run", "-i", "--rm", - "-e", "EXPRESS_SERVER_URL=http://host.docker.internal:3000", - "-e", "ENABLE_CANVAS_SYNC=true", - "ghcr.io/yctimlin/mcp_excalidraw:latest" - ] - } - } -} -``` - ---- - -### Notes - -- **Docker networking**: Use `host.docker.internal` to reach the canvas server running on your host machine. On Linux, you may need `--add-host=host.docker.internal:host-gateway` or use `172.17.0.1`. -- **Canvas server**: Must be running before the MCP server connects. Start it with `npm run canvas` (local) or `docker run -d -p 3000:3000 ghcr.io/yctimlin/mcp_excalidraw-canvas:latest` (Docker). -- **Absolute paths**: When using local node setup, replace `/absolute/path/to/mcp_excalidraw` with the actual path where you cloned and built the repo. -- **In-memory storage**: The canvas server stores elements in memory. Restarting the server will clear all elements. Use the export/import scripts if you need persistence. +Each tenant can have multiple projects (collections of elements). Use the `list_projects` and `switch_project` MCP tools, or manage via the REST API. ## Agent Skill (Optional) This repo includes a skill at `skills/excalidraw-skill/` that provides: -- **Workflow playbook** (`SKILL.md`): step-by-step guidance for drawing, refining, and exporting diagrams -- **Cheatsheet** (`references/cheatsheet.md`): MCP tool and REST API reference +- **Workflow playbook** (`SKILL.md`): step-by-step guidance for drawing, refining, and exporting diagrams — including an iterative write-check-review cycle, sizing rules, color palettes, and anti-patterns +- **Cheatsheet** (`references/cheatsheet.md`): MCP tool and REST API reference for all 32 tools - **Helper scripts** (`scripts/*.cjs`): export, import, clear, healthcheck, CRUD operations The skill complements the MCP server by giving your AI agent structured workflows to follow. -### Install The Skill (Codex CLI example) +### Install the Skill + +Copy the skill folder to your agent's skill directory: ```bash -mkdir -p ~/.codex/skills -cp -R skills/excalidraw-skill ~/.codex/skills/excalidraw-skill -``` - -To update an existing installation, remove the old folder first (`rm -rf ~/.codex/skills/excalidraw-skill`) then re-copy. - -### Install The Skill (Claude Code) - -**User-level** (available across all your projects): -```bash +# Claude Code mkdir -p ~/.claude/skills cp -R skills/excalidraw-skill ~/.claude/skills/excalidraw-skill -``` -**Project-level** (scoped to a specific project, can be committed to the repo): -```bash -mkdir -p /path/to/your/project/.claude/skills -cp -R skills/excalidraw-skill /path/to/your/project/.claude/skills/excalidraw-skill -``` +# Cursor +mkdir -p ~/.cursor/skills +cp -R skills/excalidraw-skill ~/.cursor/skills/excalidraw-skill -Then invoke the skill in Claude Code with `/excalidraw-skill`. +# Codex CLI +mkdir -p ~/.codex/skills +cp -R skills/excalidraw-skill ~/.codex/skills/excalidraw-skill + +# Or any agent that supports a skills directory +cp -R skills/excalidraw-skill /path/to/your/agent/skills/ +``` To update an existing installation, remove the old folder first then re-copy. -### Use The Skill Scripts +### Use the Skill Scripts All scripts respect `EXPRESS_SERVER_URL` (default `http://localhost:3000`) or accept `--url`. @@ -412,16 +355,16 @@ EXPRESS_SERVER_URL=http://127.0.0.1:3000 node skills/excalidraw-skill/scripts/ex EXPRESS_SERVER_URL=http://127.0.0.1:3000 node skills/excalidraw-skill/scripts/import-elements.cjs --in diagram.elements.json --mode batch ``` -### When The Skill Is Useful +### When the Skill Is Useful -- Repository workflow: export elements as JSON, commit it, and re-import later. -- Reliable refactors: clear + re-import in `sync` mode to make canvas match a file. -- Automated smoke tests: create/update/delete a known element to validate a deployment. -- Repeatable diagrams: keep a library of element JSON snippets and import them. +- **Repository workflow**: export elements as JSON, commit it, and re-import later +- **Reliable refactors**: clear + re-import in `sync` mode to make canvas match a file +- **Automated smoke tests**: create/update/delete a known element to validate a deployment +- **Repeatable diagrams**: keep a library of element JSON snippets and import them See `skills/excalidraw-skill/SKILL.md` and `skills/excalidraw-skill/references/cheatsheet.md`. -## MCP Tools (26 Total) +## MCP Tools (32 Total) | Category | Tools | |---|---| @@ -433,64 +376,106 @@ See `skills/excalidraw-skill/SKILL.md` and `skills/excalidraw-skill/references/c | **Viewport** | `set_viewport` | | **Design Guide** | `read_diagram_guide` | | **Resources** | `get_resource` | +| **Search & History** | `search_elements`, `element_history` | +| **Multi-Tenancy** | `list_tenants`, `switch_tenant` | +| **Projects** | `list_projects`, `switch_project` | Full schemas are discoverable via `tools/list` or in `skills/excalidraw-skill/references/cheatsheet.md`. ## Testing -### Canvas Smoke Test (HTTP) +### Health check ```bash curl http://localhost:3000/health ``` -### MCP Smoke Test (MCP Inspector) +### MCP Inspector List tools: ```bash npx @modelcontextprotocol/inspector --cli \ - -e EXPRESS_SERVER_URL=http://localhost:3000 \ - -e ENABLE_CANVAS_SYNC=true -- \ + -e CANVAS_PORT=3000 -- \ node dist/index.js --method tools/list ``` Create a rectangle: ```bash npx @modelcontextprotocol/inspector --cli \ - -e EXPRESS_SERVER_URL=http://localhost:3000 \ - -e ENABLE_CANVAS_SYNC=true -- \ + -e CANVAS_PORT=3000 -- \ node dist/index.js --method tools/call --tool-name create_element \ --tool-arg type=rectangle --tool-arg x=100 --tool-arg y=100 \ --tool-arg width=300 --tool-arg height=200 ``` -### Frontend Screenshots (agent-browser) +### Frontend Screenshots -If you use `agent-browser` for UI checks: +If you use a browser automation tool for UI checks: ```bash -agent-browser install -agent-browser open http://127.0.0.1:3000 -agent-browser wait --load networkidle -agent-browser screenshot /tmp/canvas.png +# Open the canvas and take a screenshot for verification +open http://127.0.0.1:3000 +# Or use agent-browser, Playwright, Puppeteer, etc. ``` ## Troubleshooting -- Canvas not updating: confirm `EXPRESS_SERVER_URL` points at the running canvas server. -- Updates/deletes fail after batch creation: ensure you are on a build that includes the batch id preservation fix (merged via PR #34). +- **Canvas not loading**: Ensure `CANVAS_PORT` isn't occupied by another process. Check `lsof -i :3000`. +- **Canvas not updating**: Confirm the MCP process is running and the browser is connected (check the status dot in the header). +- **Wrong workspace shown**: The MCP uses `server.listRoots()` to detect the workspace. Restart your MCP client if the workspace changed. +- **Elements missing after restart**: Check `~/.excalidraw-mcp/excalidraw.db` exists. If you previously ran the upstream (in-memory) version, data wasn't persisted. +- **Port conflict with multiple instances**: Set different `CANVAS_PORT` values for each workspace, or rely on multi-tenancy (same port, different tenants). +- **Updates/deletes fail after batch creation**: Ensure you are on a build that includes the batch id preservation fix. ## Known Issues / TODO -All previously listed bugs have been fixed in v2.0. Remaining items: +All previously listed bugs from the upstream have been fixed. Remaining items: -- [ ] **Persistent storage**: Elements are stored in-memory — restarting the server clears everything. Use `export_scene` / snapshots as a workaround. -- [ ] **Image export requires a browser**: `export_to_image` and `get_canvas_screenshot` rely on the frontend doing the actual rendering. The canvas UI must be open in a browser. +- [ ] **Image export requires a browser**: `export_to_image` and `get_canvas_screenshot` rely on the frontend rendering. The canvas UI must be open in a browser. +- [ ] **`export_to_excalidraw_url` blocked**: Organizations that block `excalidraw.com` cannot use shareable URL export. Use `export_scene` for local `.excalidraw` files instead. Contributions welcome! ## Development ```bash -npm run type-check -npm run build +# Type check +pnpm run type-check + +# Full build (frontend + server) +pnpm run build + +# Dev mode (watch) +pnpm run dev ``` + +### Database + +SQLite database: `~/.excalidraw-mcp/excalidraw.db` + +Override with `EXCALIDRAW_DB_PATH` environment variable. + +### REST API + +The canvas server exposes a REST API alongside the WebSocket interface: + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/health` | Health check | +| GET | `/api/elements` | List all elements | +| POST | `/api/elements` | Create an element | +| PUT | `/api/elements/:id` | Update an element | +| DELETE | `/api/elements/:id` | Delete an element | +| POST | `/api/elements/sync` | Sync all elements (bulk upsert) | +| GET | `/api/tenants` | List all tenants | +| GET | `/api/tenant/active` | Get the active tenant | +| PUT | `/api/tenant/active` | Set the active tenant | + +All endpoints accept an `X-Tenant-Id` header for per-request tenant scoping. + +## Credits + +This project is forked from [yctimlin/mcp_excalidraw](https://github.com/yctimlin/mcp_excalidraw) — an excellent Excalidraw MCP server with a live canvas, 26 tools, real-time WebSocket sync, Mermaid conversion, and a comprehensive agent skill. Full credit to [@yctimlin](https://github.com/yctimlin) for the original design and implementation. + +This fork adds SQLite persistence, multi-tenancy, auto-sync, embedded canvas lifecycle, and workspace management on top of that foundation. + +Licensed under [MIT](LICENSE). diff --git a/claude_desktop_config.json b/claude_desktop_config.json index 5b4dc4d..aa7766b 100644 --- a/claude_desktop_config.json +++ b/claude_desktop_config.json @@ -1,8 +1,11 @@ { "mcpServers": { - "mcp_excalidraw": { - "command": "npx", - "args": ["-y", "excalidraw-mcp"] + "excalidraw-canvas": { + "command": "node", + "args": ["/absolute/path/to/mcp-excalidraw-local/dist/index.js"], + "env": { + "CANVAS_PORT": "3000" + } } } -} \ No newline at end of file +} diff --git a/demo.gif b/demo.gif deleted file mode 100644 index 30c5fbe..0000000 Binary files a/demo.gif and /dev/null differ diff --git a/docker-compose.yml b/docker-compose.yml index 4542996..d0f2a4d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: build: context: . dockerfile: Dockerfile.canvas - image: mcp-excalidraw-canvas:latest + image: sanjibdevnath/mcp-excalidraw-local-canvas:latest container_name: mcp-excalidraw-canvas ports: - "3000:3000" @@ -40,7 +40,7 @@ services: build: context: . dockerfile: Dockerfile - image: mcp-excalidraw:latest + image: sanjibdevnath/mcp-excalidraw-local:latest container_name: mcp-excalidraw-mcp stdin_open: true tty: true diff --git a/docs/screenshots/canvas-ui.png b/docs/screenshots/canvas-ui.png new file mode 100644 index 0000000..c00013a Binary files /dev/null and b/docs/screenshots/canvas-ui.png differ diff --git a/docs/screenshots/workspace-switcher.png b/docs/screenshots/workspace-switcher.png new file mode 100644 index 0000000..83919f5 Binary files /dev/null and b/docs/screenshots/workspace-switcher.png differ diff --git a/frontend/index.html b/frontend/index.html index f87046b..12e5a1b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,6 +13,7 @@ } .header { + position: relative; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 10px 20px; @@ -106,6 +107,7 @@ width: 100%; position: relative; } + .api-panel { position: fixed; @@ -232,48 +234,181 @@ margin-bottom: 10px; } - /* Sync Controls Styles */ - .sync-controls { + /* Button group — joined buttons with shared style */ + .btn-group { + display: inline-flex; + border-radius: 6px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .btn-group .btn-group-item { + border-radius: 0; + border: none; + padding: 8px 14px; + font-size: 13px; + font-weight: 500; + background: #4caf50; + color: #fff; + transition: background 0.2s; + border-right: 1px solid rgba(255,255,255,0.18); + } + .btn-group .btn-group-item:last-child { border-right: none; } + .btn-group .btn-group-item:hover:not(:disabled) { background: #43a047; } + .btn-group .btn-group-item:disabled { opacity: 0.55; cursor: not-allowed; } + .btn-group .btn-group-item.btn-group-busy { opacity: 0.7; cursor: wait; } + + /* Floating toast — centered in the header */ + .toast { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background: #333; + color: #fff; + font-size: 13px; + font-weight: 500; + padding: 5px 16px; + border-radius: 999px; + pointer-events: none; + white-space: nowrap; + animation: toast-in 0.25s ease, toast-out 0.4s ease 1.6s forwards; + z-index: 10; + } + + @keyframes toast-in { + from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } + to { opacity: 1; transform: translate(-50%, -50%) scale(1); } + } + @keyframes toast-out { + from { opacity: 1; } + to { opacity: 0; } + } + + /* Header left group */ + .header-left { display: flex; align-items: center; gap: 10px; } + .header-left h1 { margin: 0; color: #333; font-size: 24px; } - .btn-loading { - position: relative; - } - - .spinner { - display: inline-block; - width: 12px; - height: 12px; - border: 2px solid #ffffff40; - border-top: 2px solid #ffffff; - border-radius: 50%; - animation: spin 1s linear infinite; - margin-right: 5px; - } - - @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } - - .sync-status { + /* Clickable tenant badge — opens workspace switcher */ + .tenant-badge-btn { font-size: 12px; - min-width: 100px; + font-weight: 600; + background: #e9ecef; + color: #495057; + padding: 4px 12px; + border-radius: 999px; + white-space: nowrap; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + border: 1px solid transparent; + transition: background 0.15s, border-color 0.15s; + } + .tenant-badge-btn:hover { + background: #dee2e6; + border-color: #ced4da; + } + .tenant-label { + color: #868e96; + font-weight: 400; } - .sync-success { + /* Menu overlay + panel */ + .menu-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.15); + z-index: 2000; + } + .menu-panel { + position: absolute; + top: 56px; + left: 100px; + width: 320px; + max-height: calc(100vh - 80px); + background: #fff; + border-radius: 10px; + box-shadow: 0 8px 30px rgba(0,0,0,0.18); + overflow: hidden; + display: flex; + flex-direction: column; + } + .menu-header { + padding: 14px 16px 10px; + font-size: 13px; + font-weight: 700; + color: #888; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid #f0f0f0; + } + .menu-search-wrap { + padding: 8px 10px 4px; + } + .menu-search { + width: 100%; + padding: 7px 10px; + font-size: 13px; + border: 1px solid #ddd; + border-radius: 6px; + outline: none; + box-sizing: border-box; + transition: border-color 0.15s; + } + .menu-search:focus { + border-color: #4caf50; + } + .menu-list { + overflow-y: auto; + max-height: 320px; + padding: 6px; + } + .menu-item { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + padding: 10px 12px; + border: none; + background: none; + border-radius: 6px; + cursor: pointer; + text-align: left; + font-size: 14px; + position: relative; + transition: background 0.15s; + } + .menu-item:hover { background: #f5f5f5; } + .menu-item-active { background: #e8f5e9; } + .menu-item-active:hover { background: #c8e6c9; } + .menu-item-name { + font-weight: 600; + color: #333; + } + .menu-item-path { + font-size: 11px; + color: #999; + margin-top: 2px; + word-break: break-all; + } + .menu-item-check { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); color: #4caf50; + font-weight: 700; + font-size: 16px; } - - .sync-error { - color: #f44336; - } - - .sync-time { - color: #666; + .menu-empty { + padding: 20px; + text-align: center; + color: #aaa; + font-size: 13px; } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 28a7e7a..20aed02 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -70,7 +70,7 @@ interface ApiResponse { message?: string; } -type SyncStatus = 'idle' | 'syncing' | 'success' | 'error'; +type SyncStatus = 'idle' | 'syncing'; // Helper function to clean elements for Excalidraw const cleanElementForExcalidraw = (element: ServerElement): Partial => { @@ -134,14 +134,52 @@ const validateAndFixBindings = (elements: Partial[]): Partial }); } +interface TenantInfo { + id: string; + name: string; + workspace_path: string; +} + function App(): JSX.Element { const [excalidrawAPI, setExcalidrawAPI] = useState(null) const [isConnected, setIsConnected] = useState(false) const websocketRef = useRef(null) - // Sync state management + // Sync state const [syncStatus, setSyncStatus] = useState('idle') - const [lastSyncTime, setLastSyncTime] = useState(null) + const [autoSave, setAutoSave] = useState(() => { + const stored = localStorage.getItem('excalidraw-autosave') + return stored === null ? true : stored === 'true' + }) + const isSyncingRef = useRef(false) + const debounceTimerRef = useRef | null>(null) + const lastSyncedHashRef = useRef('') + + const DEBOUNCE_MS = 3000 + + // Tenant state + const [activeTenant, setActiveTenant] = useState(null) + const activeTenantIdRef = useRef(null) + const [tenantList, setTenantList] = useState([]) + const [menuOpen, setMenuOpen] = useState(false) + const [tenantSearch, setTenantSearch] = useState('') + const searchInputRef = useRef(null) + + // Keep ref in sync so closures (WebSocket handlers) always see latest tenant + useEffect(() => { + activeTenantIdRef.current = activeTenant?.id ?? null + }, [activeTenant]) + + // Build headers with tenant ID for all fetch calls to the backend + const tenantHeaders = (extra?: Record): Record => { + const headers: Record = { + 'Content-Type': 'application/json', + ...extra + } + const tid = activeTenantIdRef.current + if (tid) headers['X-Tenant-Id'] = tid + return headers + } // WebSocket connection useEffect(() => { @@ -165,15 +203,71 @@ function App(): JSX.Element { } }, [excalidrawAPI, isConnected]) + const computeElementHash = (elements: readonly { id: string; version: number }[]): string => { + let h = String(elements.length) + for (let i = 0; i < elements.length; i++) { + h += elements[i].id + h += elements[i].version + } + return h + } + + // Persist auto-save preference and cancel pending timer when toggled off + const toggleAutoSave = () => { + setAutoSave(prev => { + const next = !prev + localStorage.setItem('excalidraw-autosave', String(next)) + if (!next && debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current) + debounceTimerRef.current = null + } + return next + }) + } + + // Clean up debounce timer on unmount + useEffect(() => { + return () => { + if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current) + } + }, []) + + // Trailing debounce: resets on every change, fires after user is idle. + // Only active when auto-save is on. + const handleCanvasChange = (): void => { + if (!autoSave) return + + if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current) + + debounceTimerRef.current = setTimeout(() => { + if (!excalidrawAPI || isSyncingRef.current) return + + const elements = excalidrawAPI.getSceneElements() + const hash = computeElementHash(elements) + if (hash === lastSyncedHashRef.current) return + + syncToBackend() + }, DEBOUNCE_MS) + } + const loadExistingElements = async (): Promise => { try { - const response = await fetch('/api/elements') + const response = await fetch('/api/elements', { headers: tenantHeaders() }) const result: ApiResponse = await response.json() if (result.success && result.elements && result.elements.length > 0) { const cleanedElements = result.elements.map(cleanElementForExcalidraw) - const convertedElements = convertToExcalidrawElements(cleanedElements, { regenerateIds: false }) - excalidrawAPI?.updateScene({ elements: convertedElements }) + // Elements with containerId are in Excalidraw native format (from a + // previous sync before the normalization fix). Pass them directly — + // convertToExcalidrawElements would re-create bound text and break layout. + const hasNativeFormat = cleanedElements.some((el: any) => el.containerId) + if (hasNativeFormat) { + const validated = validateAndFixBindings(cleanedElements) + excalidrawAPI?.updateScene({ elements: validated as any }) + } else { + const convertedElements = convertToExcalidrawElements(cleanedElements, { regenerateIds: false }) + excalidrawAPI?.updateScene({ elements: convertedElements }) + } } } catch (error) { console.error('Error loading existing elements:', error) @@ -355,7 +449,7 @@ function App(): JSX.Element { const svgString = new XMLSerializer().serializeToString(svg) await fetch('/api/export/image/result', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: tenantHeaders(), body: JSON.stringify({ requestId: data.requestId, format: 'svg', @@ -382,7 +476,7 @@ function App(): JSX.Element { } await fetch('/api/export/image/result', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: tenantHeaders(), body: JSON.stringify({ requestId: data.requestId, format: 'png', @@ -393,7 +487,7 @@ function App(): JSX.Element { console.error('Image export (FileReader) failed:', readerError) await fetch('/api/export/image/result', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: tenantHeaders(), body: JSON.stringify({ requestId: data.requestId, error: (readerError as Error).message @@ -405,7 +499,7 @@ function App(): JSX.Element { console.error('FileReader error:', reader.error) await fetch('/api/export/image/result', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: tenantHeaders(), body: JSON.stringify({ requestId: data.requestId, error: reader.error?.message || 'FileReader failed' @@ -419,7 +513,7 @@ function App(): JSX.Element { console.error('Image export failed:', exportError) await fetch('/api/export/image/result', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: tenantHeaders(), body: JSON.stringify({ requestId: data.requestId, error: (exportError as Error).message @@ -465,7 +559,7 @@ function App(): JSX.Element { await fetch('/api/viewport/result', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: tenantHeaders(), body: JSON.stringify({ requestId: data.requestId, success: true, @@ -476,7 +570,7 @@ function App(): JSX.Element { console.error('Viewport control failed:', viewportError) await fetch('/api/viewport/result', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: tenantHeaders(), body: JSON.stringify({ requestId: data.requestId, error: (viewportError as Error).message @@ -519,6 +613,27 @@ function App(): JSX.Element { } break + case 'tenant_switched': + console.log('Tenant switched:', data.tenant) + if (data.tenant) { + const incoming = data.tenant as TenantInfo + // Only reload if the switch came from an external source (MCP tool) + // and we aren't already on that tenant (UI-driven switch handles its own reload) + if (incoming.id !== activeTenantIdRef.current) { + activeTenantIdRef.current = incoming.id + setActiveTenant(incoming) + excalidrawAPI.updateScene({ + elements: [], + captureUpdate: CaptureUpdateAction.NEVER + }) + lastSyncedHashRef.current = '' + loadExistingElements() + } else { + setActiveTenant(incoming) + } + } + break + default: console.log('Unknown WebSocket message type:', data.type) } @@ -527,84 +642,192 @@ function App(): JSX.Element { } } - // Data format conversion for backend - const convertToBackendFormat = (element: ExcalidrawElement): ServerElement => { - return { - ...element - } as ServerElement + // Normalize Excalidraw native elements back to MCP format for backend storage. + // Excalidraw internally splits label text out of containers into separate text + // elements linked by containerId/boundElements. This causes text to detach on + // reload because convertToExcalidrawElements doesn't reconstruct that binding. + // Fix: merge bound text back into container label.text so the backend always + // stores MCP format that round-trips cleanly. + const normalizeForBackend = (elements: readonly ExcalidrawElement[]): ServerElement[] => { + const elementMap = new Map() + for (const el of elements) elementMap.set(el.id, el) + + // Collect IDs of text elements that are bound inside a container + const boundTextIds = new Set() + // Map containerId → text content for merging + const containerTextMap = new Map() + + for (const el of elements) { + const cid = (el as any).containerId + if (el.type === 'text' && cid && elementMap.has(cid)) { + boundTextIds.add(el.id) + containerTextMap.set(cid, { + text: (el as any).text || (el as any).originalText || '', + fontSize: (el as any).fontSize, + fontFamily: (el as any).fontFamily, + }) + } + } + + const result: ServerElement[] = [] + for (const el of elements) { + if (boundTextIds.has(el.id)) continue // skip bound text — merged into container + + const out: any = { ...el } + + // If this container has bound text, put it back as label.text + const merged = containerTextMap.get(el.id) + if (merged && merged.text) { + out.label = { text: merged.text } + if (merged.fontSize) out.fontSize = merged.fontSize + if (merged.fontFamily) out.fontFamily = merged.fontFamily + // Clean up Excalidraw-internal binding metadata + delete out.boundElements + } + + // Normalize arrow bindings from Excalidraw format back to MCP format + if (el.type === 'arrow') { + const startBinding = (el as any).startBinding + const endBinding = (el as any).endBinding + if (startBinding?.elementId) out.start = { id: startBinding.elementId } + if (endBinding?.elementId) out.end = { id: endBinding.elementId } + } + + result.push(out as ServerElement) + } + return result } - // Format sync time display - const formatSyncTime = (time: Date | null): string => { - if (!time) return '' - return time.toLocaleTimeString('zh-CN', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }) + // Toast message shown briefly in the center of the header + const [toast, setToast] = useState(null) + const toastTimerRef = useRef | null>(null) + + const showToast = (msg: string, durationMs = 2000) => { + if (toastTimerRef.current) clearTimeout(toastTimerRef.current) + setToast(msg) + toastTimerRef.current = setTimeout(() => setToast(null), durationMs) } - // Main sync function - const syncToBackend = async (): Promise => { - if (!excalidrawAPI) { - console.warn('Excalidraw API not available') + // Fetch list of tenants for the menu + const fetchTenants = async () => { + try { + const res = await fetch('/api/tenants', { headers: tenantHeaders() }) + if (!res.ok) return + const data = await res.json() + if (data.success) { + setTenantList(data.tenants) + } + } catch (err) { + console.error('Failed to fetch tenants:', err) + } + } + + // Switch active tenant via API, then reload canvas with new tenant's elements + const switchTenant = async (tenantId: string) => { + if (tenantId === activeTenantIdRef.current) { + setMenuOpen(false) return } - - setSyncStatus('syncing') - + try { - // 1. Get current elements - const currentElements = excalidrawAPI.getSceneElements() - console.log(`Syncing ${currentElements.length} elements to backend`) - - // Filter out deleted elements - const activeElements = currentElements.filter(el => !el.isDeleted) - - // 3. Convert to backend format - const backendElements = activeElements.map(convertToBackendFormat) - - // 4. Send to backend - const response = await fetch('/api/elements/sync', { - method: 'POST', + const res = await fetch('/api/tenant/active', { + method: 'PUT', + headers: tenantHeaders(), + body: JSON.stringify({ tenantId }) + }) + if (!res.ok) return + + // Update ref immediately so subsequent fetch uses the new tenant + activeTenantIdRef.current = tenantId + + // Clear the canvas before loading the new tenant's elements + excalidrawAPI?.updateScene({ + elements: [], + captureUpdate: CaptureUpdateAction.NEVER + }) + lastSyncedHashRef.current = '' + + // Update React state (will also re-sync the ref via useEffect, which is fine) + const tenant = tenantList.find(t => t.id === tenantId) + if (tenant) setActiveTenant(tenant) + + setMenuOpen(false) + + // Load elements for the newly-active tenant + const elemRes = await fetch('/api/elements', { headers: { 'Content-Type': 'application/json', - }, + 'X-Tenant-Id': tenantId + } + }) + const result: ApiResponse = await elemRes.json() + if (result.success && result.elements && result.elements.length > 0) { + const cleanedElements = result.elements.map(cleanElementForExcalidraw) + const hasNativeFormat = cleanedElements.some((el: any) => el.containerId) + if (hasNativeFormat) { + const validated = validateAndFixBindings(cleanedElements) + excalidrawAPI?.updateScene({ elements: validated as any }) + } else { + const convertedElements = convertToExcalidrawElements(cleanedElements, { regenerateIds: false }) + excalidrawAPI?.updateScene({ elements: convertedElements }) + } + } + + showToast('Workspace switched') + } catch (err) { + console.error('Failed to switch tenant:', err) + } + } + + const syncToBackend = async (): Promise => { + if (!excalidrawAPI || isSyncingRef.current) return + + isSyncingRef.current = true + setSyncStatus('syncing') + + try { + const currentElements = excalidrawAPI.getSceneElements() + const activeElements = currentElements.filter(el => !el.isDeleted) + const backendElements = normalizeForBackend(activeElements) + + const response = await fetch('/api/elements/sync', { + method: 'POST', + headers: tenantHeaders(), body: JSON.stringify({ elements: backendElements, timestamp: new Date().toISOString() }) }) - + if (response.ok) { const result: ApiResponse = await response.json() - setSyncStatus('success') - setLastSyncTime(new Date()) - console.log(`Sync successful: ${result.count} elements synced`) - - // Reset status after 2 seconds - setTimeout(() => setSyncStatus('idle'), 2000) + lastSyncedHashRef.current = computeElementHash(currentElements) + setSyncStatus('idle') + showToast('Saved') + console.log(`Sync: ${result.count} elements synced`) } else { - const error: ApiResponse = await response.json() - setSyncStatus('error') - console.error('Sync failed:', error.error) + setSyncStatus('idle') + showToast('Sync failed', 3000) + console.error('Sync failed:', (await response.json() as ApiResponse).error) } } catch (error) { - setSyncStatus('error') + setSyncStatus('idle') + showToast('Sync failed', 3000) console.error('Sync error:', error) + } finally { + isSyncingRef.current = false } } const clearCanvas = async (): Promise => { if (excalidrawAPI) { try { - // Get all current elements and delete them from backend - const response = await fetch('/api/elements') + const response = await fetch('/api/elements', { headers: tenantHeaders() }) const result: ApiResponse = await response.json() if (result.success && result.elements) { const deletePromises = result.elements.map(element => - fetch(`/api/elements/${element.id}`, { method: 'DELETE' }) + fetch(`/api/elements/${element.id}`, { method: 'DELETE', headers: tenantHeaders() }) ) await Promise.all(deletePromises) } @@ -629,44 +852,100 @@ function App(): JSX.Element {
{/* Header */}
-

Excalidraw Canvas

+
+

Excalidraw Canvas

+ {activeTenant && ( + + )} +
+ + {toast &&
{toast}
} +
{isConnected ? 'Connected' : 'Disconnected'}
- {/* Sync Controls */} -
- + - - {/* Sync Status */} -
- {syncStatus === 'success' && ( - ✅ Synced - )} - {syncStatus === 'error' && ( - ❌ Sync Failed - )} - {lastSyncTime && syncStatus === 'idle' && ( - - Last sync: {formatSyncTime(lastSyncTime)} - - )} -
+ {/* Tenant menu overlay */} + {menuOpen && (() => { + const q = tenantSearch.toLowerCase() + const filtered = q + ? tenantList.filter(t => t.name.toLowerCase().includes(q) || t.workspace_path.toLowerCase().includes(q)) + : tenantList + return ( +
setMenuOpen(false)}> +
e.stopPropagation()}> +
Workspaces
+
+ setTenantSearch(e.target.value)} + /> +
+
+ {filtered.map(t => ( + + ))} + {filtered.length === 0 &&
No matching workspaces
} +
+
+
+ ) + })()} + {/* Canvas Container */}
diff --git a/package-lock.json b/package-lock.json index 34436ef..2e47c16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@excalidraw/excalidraw": "^0.18.0", "@excalidraw/mermaid-to-excalidraw": "^1.1.3", "@modelcontextprotocol/sdk": "latest", + "better-sqlite3": "^12.6.2", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", @@ -28,6 +29,7 @@ "mcp-excalidraw-server": "dist/index.js" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node": "^20.19.7", @@ -2583,6 +2585,16 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -3173,6 +3185,40 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", + "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3185,6 +3231,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -3302,6 +3368,30 @@ "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==", + "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/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3456,6 +3546,12 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4258,6 +4354,30 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/delaunator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", @@ -4295,6 +4415,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -4386,6 +4515,15 @@ "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==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -4513,6 +4651,15 @@ "node": ">=20.0.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -4636,6 +4783,12 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4726,6 +4879,12 @@ "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==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4823,6 +4982,12 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/globals": { "version": "15.15.0", "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", @@ -4921,6 +5086,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/image-blob-reduce": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/image-blob-reduce/-/image-blob-reduce-3.0.1.tgz", @@ -4942,6 +5127,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -5871,6 +6062,33 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -5937,6 +6155,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -5946,6 +6170,30 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -6264,6 +6512,32 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6288,6 +6562,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6358,6 +6642,21 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -6880,6 +7179,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -6971,6 +7315,15 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stylis": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", @@ -6990,6 +7343,34 @@ "node": ">=8" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -7102,6 +7483,18 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tunnel-rat": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", diff --git a/package.json b/package.json index d63eb4e..e830186 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "mcp-excalidraw-server", - "version": "1.0.2", - "description": "Advanced MCP server for Excalidraw with real-time canvas, WebSocket sync, and comprehensive diagram management", + "name": "@sanjibdevnath/mcp-excalidraw-local", + "version": "3.0.0", + "description": "Fully local MCP server for Excalidraw with SQLite persistence, multi-tenancy, auto-sync, real-time canvas, and 32 tools", "main": "dist/index.js", "type": "module", "bin": { - "mcp-excalidraw-server": "dist/index.js" + "mcp-excalidraw-local": "dist/index.js" }, "scripts": { "start": "npm run build:server && node dist/index.js", @@ -24,6 +24,7 @@ "@excalidraw/excalidraw": "^0.18.0", "@excalidraw/mermaid-to-excalidraw": "^1.1.3", "@modelcontextprotocol/sdk": "latest", + "better-sqlite3": "^12.6.2", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", @@ -37,6 +38,7 @@ "zod-to-json-schema": "^3.22.3" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node": "^20.19.7", @@ -60,21 +62,35 @@ "real-time", "websocket", "visualization", - "claude", - "ai-tools" + "sqlite", + "multi-tenancy", + "self-hosted", + "local" ], "author": { - "name": "yctimlin", - "email": "c22647809@gmail.com" + "name": "sanjibdevnathlabs" }, + "contributors": [ + { + "name": "yctimlin", + "email": "c22647809@gmail.com", + "url": "https://github.com/yctimlin" + } + ], "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/yctimlin/mcp_excalidraw.git" + "url": "https://github.com/sanjibdevnathlabs/mcp-excalidraw-local.git" }, - "homepage": "https://github.com/yctimlin/mcp_excalidraw#readme", + "homepage": "https://github.com/sanjibdevnathlabs/mcp-excalidraw-local#readme", "bugs": { - "url": "https://github.com/yctimlin/mcp_excalidraw/issues" + "url": "https://github.com/sanjibdevnathlabs/mcp-excalidraw-local/issues" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "better-sqlite3", + "esbuild" + ] }, "engines": { "node": ">=18.0.0" @@ -84,9 +100,8 @@ "registry": "https://registry.npmjs.org/" }, "files": [ - "src/**/*", "dist/**/*", - "*.d.ts", + "skills/**/*", "README.md", "LICENSE" ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..15b290b --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5507 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@excalidraw/excalidraw': + specifier: ^0.18.0 + version: 0.18.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@excalidraw/mermaid-to-excalidraw': + specifier: ^1.1.3 + version: 1.1.4(react@18.3.1) + '@modelcontextprotocol/sdk': + specifier: latest + version: 1.26.0(zod@3.25.76) + better-sqlite3: + specifier: ^12.6.2 + version: 12.6.2 + cors: + specifier: ^2.8.5 + version: 2.8.6 + dotenv: + specifier: ^16.3.1 + version: 16.6.1 + express: + specifier: ^4.18.2 + version: 4.22.1 + mermaid: + specifier: ^11.12.1 + version: 11.12.3 + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + winston: + specifier: ^3.11.0 + version: 3.19.0 + ws: + specifier: ^8.14.2 + version: 8.19.0 + zod: + specifier: ^3.22.4 + version: 3.25.76 + zod-to-json-schema: + specifier: ^3.22.3 + version: 3.25.1(zod@3.25.76) + devDependencies: + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^4.17.21 + version: 4.17.25 + '@types/node': + specifier: ^20.19.7 + version: 20.19.33 + '@types/react': + specifier: ^18.3.3 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.28) + '@types/ws': + specifier: ^8.5.10 + version: 8.18.1 + '@vitejs/plugin-react': + specifier: ^4.6.0 + version: 4.7.0(vite@6.4.1(@types/node@20.19.33)(sass@1.51.0)) + concurrently: + specifier: ^9.2.0 + version: 9.2.1 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vite: + specifier: ^6.3.5 + version: 6.4.1(@types/node@20.19.33)(sass@1.51.0) + +packages: + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.6': + resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@braintree/sanitize-url@6.0.2': + resolution: {integrity: sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==} + + '@braintree/sanitize-url@6.0.4': + resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + + '@chevrotain/cst-dts-gen@11.1.1': + resolution: {integrity: sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==} + + '@chevrotain/gast@11.1.1': + resolution: {integrity: sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==} + + '@chevrotain/regexp-to-ast@11.1.1': + resolution: {integrity: sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==} + + '@chevrotain/types@11.1.1': + resolution: {integrity: sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==} + + '@chevrotain/utils@11.1.1': + resolution: {integrity: sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@dabh/diagnostics@2.0.8': + resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@excalidraw/excalidraw@0.18.0': + resolution: {integrity: sha512-QkIiS+5qdy8lmDWTKsuy0sK/fen/LRDtbhm2lc2xcFcqhv2/zdg95bYnl+wnwwXGHo7kEmP65BSiMHE7PJ3Zpw==} + peerDependencies: + react: ^17.0.2 || ^18.2.0 || ^19.0.0 + react-dom: ^17.0.2 || ^18.2.0 || ^19.0.0 + + '@excalidraw/laser-pointer@1.3.1': + resolution: {integrity: sha512-psA1z1N2qeAfsORdXc9JmD2y4CmDwmuMRxnNdJHZexIcPwaNEyIpNcelw+QkL9rz9tosaN9krXuKaRqYpRAR6g==} + + '@excalidraw/markdown-to-text@0.1.2': + resolution: {integrity: sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==} + + '@excalidraw/mermaid-to-excalidraw@1.1.2': + resolution: {integrity: sha512-hAFv/TTIsOdoy0dL5v+oBd297SQ+Z88gZ5u99fCIFuEMHfQuPgLhU/ztKhFSTs7fISwVo6fizny/5oQRR3d4tQ==} + + '@excalidraw/mermaid-to-excalidraw@1.1.4': + resolution: {integrity: sha512-RfuNMLLBhlqOUqRmB9LuuXEhD7AKGXJJOSrjcDHBOxnuY9E9Rlj5HGCqTLZfEu7NWzpTb+24Sq8BORoKhMXaPw==} + + '@excalidraw/random-username@1.1.0': + resolution: {integrity: sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA==} + engines: {node: '>=10'} + + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} + + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} + + '@floating-ui/react-dom@2.1.7': + resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mermaid-js/parser@1.0.0': + resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==} + + '@modelcontextprotocol/sdk@1.26.0': + resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@radix-ui/primitive@1.0.0': + resolution: {integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==} + + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + + '@radix-ui/react-arrow@1.1.2': + resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.0.1': + resolution: {integrity: sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-compose-refs@1.0.0': + resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.0.0': + resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.0.0': + resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-dismissable-layer@1.1.5': + resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.2': + resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.0.0': + resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-popover@1.1.6': + resolution: {integrity: sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.2': + resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.4': + resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.0.0': + resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@1.0.1': + resolution: {integrity: sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-primitive@2.0.2': + resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.0.2': + resolution: {integrity: sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-slot@1.0.1': + resolution: {integrity: sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-slot@1.1.2': + resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tabs@1.0.2': + resolution: {integrity: sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-use-callback-ref@1.0.0': + resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.0.0': + resolution: {integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.0.0': + resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + + '@so-ric/colorspace@1.1.6': + resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} + + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/mdast@3.0.15': + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@20.19.33': + resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} + hasBin: true + + better-sqlite3@12.6.2: + resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-fs-access@0.29.1: + resolution: {integrity: sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw==} + + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001770: + resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==} + + canvas-roundrect-polyfill@0.0.1: + resolution: {integrity: sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.1.1: + resolution: {integrity: sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@1.1.1: + resolution: {integrity: sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-convert@3.1.3: + resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} + engines: {node: '>=14.6'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-name@2.1.0: + resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} + engines: {node: '>=12.20'} + + color-string@2.1.4: + resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} + engines: {node: '>=18'} + + color@5.0.3: + resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} + engines: {node: '>=18'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + crc-32@0.3.0: + resolution: {integrity: sha512-kucVIjOmMc1f0tv53BJ/5WIX+MGLcKuoBhnGqQrgKJNqLByb/sVMWfW/Aw6hw0jgcqjJ2pi9E5y32zOIpaUlsA==} + engines: {node: '>=0.8'} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.1: + resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.10: + resolution: {integrity: sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==} + + dagre-d3-es@7.0.13: + resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + diff@5.2.2: + resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} + engines: {node: '>=0.3.1'} + + dompurify@3.1.6: + resolution: {integrity: sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==} + + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} + + elkjs@0.9.3: + resolution: {integrity: sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es6-promise-pool@2.5.0: + resolution: {integrity: sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA==} + engines: {node: '>=0.10.0'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fractional-indexing@3.2.0: + resolution: {integrity: sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==} + engines: {node: ^14.13.1 || >=16.0.0} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + fuzzy@0.1.3: + resolution: {integrity: sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==} + engines: {node: '>= 0.6.0'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glur@1.1.2: + resolution: {integrity: sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hono@4.11.10: + resolution: {integrity: sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + image-blob-reduce@3.0.1: + resolution: {integrity: sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q==} + + immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + + jotai-scope@0.7.2: + resolution: {integrity: sha512-Gwed97f3dDObrO43++2lRcgOqw4O2sdr4JCjP/7eHK1oPACDJ7xKHGScpJX9XaflU+KBHXF+VhwECnzcaQiShg==} + peerDependencies: + jotai: '>=2.9.2' + react: '>=17.0.0' + + jotai@2.11.0: + resolution: {integrity: sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=17.0.0' + react: '>=17.0.0' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + katex@0.16.28: + resolution: {integrity: sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + langium@4.2.1: + resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==} + engines: {node: '>=20.10.0', npm: '>=10.2.3'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-from-markdown@1.3.1: + resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} + + mdast-util-to-string@3.2.0: + resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mermaid@10.9.3: + resolution: {integrity: sha512-V80X1isSEvAewIL3xhmz/rVmc27CVljcsbWxkxlWJWY/1kQa4XOABqpDl2qQLGKzpKm6WbTfUEKImBlUfFYArw==} + + mermaid@10.9.4: + resolution: {integrity: sha512-VIG2B0R9ydvkS+wShA8sXqkzfpYglM2Qwj7VyUeqzNVqSGPoP/tcaUr3ub4ESykv8eqQJn3p99bHNvYdg3gCHQ==} + + mermaid@11.12.3: + resolution: {integrity: sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromark-core-commonmark@1.1.0: + resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} + + micromark-factory-destination@1.1.0: + resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} + + micromark-factory-label@1.1.0: + resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} + + micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + + micromark-factory-title@1.1.0: + resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} + + micromark-factory-whitespace@1.1.0: + resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} + + micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + + micromark-util-chunked@1.1.0: + resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} + + micromark-util-classify-character@1.1.0: + resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} + + micromark-util-combine-extensions@1.1.0: + resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} + + micromark-util-decode-numeric-character-reference@1.1.0: + resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} + + micromark-util-decode-string@1.1.0: + resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} + + micromark-util-encode@1.1.0: + resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} + + micromark-util-html-tag-name@1.2.0: + resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} + + micromark-util-normalize-identifier@1.1.0: + resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} + + micromark-util-resolve-all@1.1.0: + resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} + + micromark-util-sanitize-uri@1.2.0: + resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} + + micromark-util-subtokenize@1.1.0: + resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} + + micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + + micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + + micromark@3.2.0: + resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multimath@2.0.0: + resolution: {integrity: sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@3.3.3: + resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@4.0.2: + resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + open-color@1.9.1: + resolution: {integrity: sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw==} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + pako@2.0.3: + resolution: {integrity: sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-freehand@1.2.0: + resolution: {integrity: sha512-h/0ikF1M3phW7CwpZ5MMvKnfpHficWoOEyr//KVNTxV4F6deRK1eYMtHyBKEAKFK0aXIEUK9oBvlF6PNXMDsAw==} + + pica@7.1.1: + resolution: {integrity: sha512-WY73tMvNzXWEld2LicT9Y260L43isrZ85tPuqRyvtkljSDLmnNFQmZICt4xUJMVulmcc6L9O7jbBrtx3DOz/YQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + png-chunk-text@1.0.0: + resolution: {integrity: sha512-DEROKU3SkkLGWNMzru3xPVgxyd48UGuMSZvioErCure6yhOc/pRH2ZV+SEn7nmaf7WNf3NdIpH+UTrRdKyq9Lw==} + + png-chunks-encode@1.0.0: + resolution: {integrity: sha512-J1jcHgbQRsIIgx5wxW9UmCymV3wwn4qCCJl6KYgEU/yHCh/L2Mwq/nMOkRPtmV79TLxRZj5w3tH69pvygFkDqA==} + + png-chunks-extract@1.0.0: + resolution: {integrity: sha512-ZiVwF5EJ0DNZyzAqld8BP1qyJBaGOFaq9zl579qfbkcmOwWLLO4I9L8i2O4j3HkI6/35i0nKG2n+dZplxiT89Q==} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-curve@1.0.1: + resolution: {integrity: sha512-3nmX4/LIiyuwGLwuUrfhTlDeQFlAhi7lyK/zcRNGhalwapDWgAGR82bUpmn2mA03vII3fvNCG8jAONzKXwpxAg==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + pwacompat@2.0.17: + resolution: {integrity: sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w==} + + qs@6.14.2: + resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + engines: {node: '>=0.6'} + + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-split@2.0.14: + resolution: {integrity: sha512-bKWydgMgaKTg/2JGQnaJPg51T6dmumTWZppFgEbbY0Fbme0F5TuatAScCLaqommbGQQf/ZT1zaejuPDriscISA==} + peerDependencies: + react: '*' + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + roughjs@4.6.4: + resolution: {integrity: sha512-s6EZ0BntezkFYMf/9mGn7M8XGIoaav9QQBCnJROWB3brUWQ683Q2LbRD/hq0Z3bAJ/9NVpU/5LpiTWvQMyLDhw==} + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass@1.51.0: + resolution: {integrity: sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==} + engines: {node: '>=12.0.0'} + hasBin: true + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + sliced@1.0.1: + resolution: {integrity: sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + split.js@1.6.5: + resolution: {integrity: sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unist-util-stringify-position@3.0.3: + resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + uvu@0.5.6: + resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} + engines: {node: '>=8'} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + 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 + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + web-worker@1.5.0: + resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} + + webworkify@1.5.0: + resolution: {integrity: sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.19.0: + resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} + engines: {node: '>= 12.0.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/runtime@7.28.6': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@braintree/sanitize-url@6.0.2': {} + + '@braintree/sanitize-url@6.0.4': {} + + '@braintree/sanitize-url@7.1.2': {} + + '@chevrotain/cst-dts-gen@11.1.1': + dependencies: + '@chevrotain/gast': 11.1.1 + '@chevrotain/types': 11.1.1 + lodash-es: 4.17.23 + + '@chevrotain/gast@11.1.1': + dependencies: + '@chevrotain/types': 11.1.1 + lodash-es: 4.17.23 + + '@chevrotain/regexp-to-ast@11.1.1': {} + + '@chevrotain/types@11.1.1': {} + + '@chevrotain/utils@11.1.1': {} + + '@colors/colors@1.6.0': {} + + '@dabh/diagnostics@2.0.8': + dependencies: + '@so-ric/colorspace': 1.1.6 + enabled: 2.0.0 + kuler: 2.0.0 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@excalidraw/excalidraw@0.18.0(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@braintree/sanitize-url': 6.0.2 + '@excalidraw/laser-pointer': 1.3.1 + '@excalidraw/mermaid-to-excalidraw': 1.1.2 + '@excalidraw/random-username': 1.1.0 + '@radix-ui/react-popover': 1.1.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + browser-fs-access: 0.29.1 + canvas-roundrect-polyfill: 0.0.1 + clsx: 1.1.1 + cross-env: 7.0.3 + es6-promise-pool: 2.5.0 + fractional-indexing: 3.2.0 + fuzzy: 0.1.3 + image-blob-reduce: 3.0.1 + jotai: 2.11.0(@types/react@18.3.28)(react@18.3.1) + jotai-scope: 0.7.2(jotai@2.11.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1) + lodash.debounce: 4.0.8 + lodash.throttle: 4.1.1 + nanoid: 3.3.3 + open-color: 1.9.1 + pako: 2.0.3 + perfect-freehand: 1.2.0 + pica: 7.1.1 + png-chunk-text: 1.0.0 + png-chunks-encode: 1.0.0 + png-chunks-extract: 1.0.0 + points-on-curve: 1.0.1 + pwacompat: 2.0.17 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + roughjs: 4.6.4 + sass: 1.51.0 + tunnel-rat: 0.1.2(@types/react@18.3.28)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - immer + - supports-color + + '@excalidraw/laser-pointer@1.3.1': {} + + '@excalidraw/markdown-to-text@0.1.2': {} + + '@excalidraw/mermaid-to-excalidraw@1.1.2': + dependencies: + '@excalidraw/markdown-to-text': 0.1.2 + mermaid: 10.9.3 + nanoid: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@excalidraw/mermaid-to-excalidraw@1.1.4(react@18.3.1)': + dependencies: + '@excalidraw/markdown-to-text': 0.1.2 + mermaid: 10.9.4 + nanoid: 4.0.2 + react-split: 2.0.14(react@18.3.1) + transitivePeerDependencies: + - react + - supports-color + + '@excalidraw/random-username@1.1.0': {} + + '@floating-ui/core@1.7.4': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.5': + dependencies: + '@floating-ui/core': 1.7.4 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.10': {} + + '@hono/node-server@1.19.9(hono@4.11.10)': + dependencies: + hono: 4.11.10 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mermaid-js/parser@1.0.0': + dependencies: + langium: 4.2.1 + + '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.9(hono@4.11.10) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.11.10 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@radix-ui/primitive@1.0.0': + dependencies: + '@babel/runtime': 7.28.6 + + '@radix-ui/primitive@1.1.1': {} + + '@radix-ui/react-arrow@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-collection@1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) + '@radix-ui/react-context': 1.0.0(react@18.3.1) + '@radix-ui/react-primitive': 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@radix-ui/react-compose-refs@1.0.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + react: 18.3.1 + + '@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-context@1.0.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + react: 18.3.1 + + '@radix-ui/react-context@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-direction@1.0.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + react: 18.3.1 + + '@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-focus-scope@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-id@1.0.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@radix-ui/react-use-layout-effect': 1.0.0(react@18.3.1) + react: 18.3.1 + + '@radix-ui/react-id@1.1.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-popover@1.1.6(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.2.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.28)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-popper@1.2.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-portal@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-presence@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@radix-ui/react-presence@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-primitive@1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@radix-ui/react-slot': 1.0.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@radix-ui/react-primitive@2.0.2(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.2(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@radix-ui/react-roving-focus@1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@radix-ui/primitive': 1.0.0 + '@radix-ui/react-collection': 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) + '@radix-ui/react-context': 1.0.0(react@18.3.1) + '@radix-ui/react-direction': 1.0.0(react@18.3.1) + '@radix-ui/react-id': 1.0.0(react@18.3.1) + '@radix-ui/react-primitive': 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@radix-ui/react-slot@1.0.1(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) + react: 18.3.1 + + '@radix-ui/react-slot@1.1.2(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-tabs@1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@radix-ui/primitive': 1.0.0 + '@radix-ui/react-context': 1.0.0(react@18.3.1) + '@radix-ui/react-direction': 1.0.0(react@18.3.1) + '@radix-ui/react-id': 1.0.0(react@18.3.1) + '@radix-ui/react-presence': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@radix-ui/react-use-callback-ref@1.0.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + react: 18.3.1 + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-controllable-state@1.0.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1) + react: 18.3.1 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-layout-effect@1.0.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + react: 18.3.1 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.28)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.28 + + '@radix-ui/rect@1.1.0': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + + '@rollup/rollup-android-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + + '@so-ric/colorspace@1.1.6': + dependencies: + color: 5.0.3 + text-hex: 1.0.0 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 20.19.33 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.33 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.33 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 20.19.33 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@4.19.8': + dependencies: + '@types/node': 20.19.33 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.8 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + + '@types/geojson@7946.0.16': {} + + '@types/http-errors@2.0.5': {} + + '@types/mdast@3.0.15': + dependencies: + '@types/unist': 2.0.11 + + '@types/mime@1.3.5': {} + + '@types/ms@2.1.0': {} + + '@types/node@20.19.33': + dependencies: + undici-types: 6.21.0 + + '@types/prop-types@15.7.15': {} + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.19.33 + + '@types/send@1.2.1': + dependencies: + '@types/node': 20.19.33 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.33 + '@types/send': 0.17.6 + + '@types/triple-beam@1.3.5': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@types/unist@2.0.11': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.19.33 + + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.33)(sass@1.51.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.4.1(@types/node@20.19.33)(sass@1.51.0) + transitivePeerDependencies: + - supports-color + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn@8.15.0: {} + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + array-flatten@1.1.1: {} + + async@3.2.6: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.9.19: {} + + better-sqlite3@12.6.2: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@1.20.4: + 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.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-fs-access@0.29.1: {} + + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001770 + electron-to-chromium: 1.5.286 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + caniuse-lite@1.0.30001770: {} + + canvas-roundrect-polyfill@0.0.1: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + character-entities@2.0.2: {} + + chevrotain-allstar@0.3.1(chevrotain@11.1.1): + dependencies: + chevrotain: 11.1.1 + lodash-es: 4.17.23 + + chevrotain@11.1.1: + dependencies: + '@chevrotain/cst-dts-gen': 11.1.1 + '@chevrotain/gast': 11.1.1 + '@chevrotain/regexp-to-ast': 11.1.1 + '@chevrotain/types': 11.1.1 + '@chevrotain/utils': 11.1.1 + lodash-es: 4.17.23 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@1.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-convert@3.1.3: + dependencies: + color-name: 2.1.0 + + color-name@1.1.4: {} + + color-name@2.1.0: {} + + color-string@2.1.4: + dependencies: + color-name: 2.1.0 + + color@5.0.3: + dependencies: + color-convert: 3.1.3 + color-string: 2.1.4 + + commander@7.2.0: {} + + commander@8.3.0: {} + + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + confbox@0.1.8: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.0.7: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + crc-32@0.3.0: {} + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.1 + + cytoscape-fcose@2.2.0(cytoscape@3.33.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.1 + + cytoscape@3.33.1: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.10: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.23 + + dagre-d3-es@7.0.13: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.23 + + data-uri-to-buffer@4.0.1: {} + + dayjs@1.11.19: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + diff@5.2.2: {} + + dompurify@3.1.6: {} + + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.286: {} + + elkjs@0.9.3: {} + + emoji-regex@8.0.0: {} + + enabled@2.0.0: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es6-promise-pool@2.5.0: {} + + esbuild@0.25.12: + 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 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + expand-template@2.0.3: {} + + express-rate-limit@8.2.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.0.1 + + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + 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.14.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fecha@4.2.3: {} + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.2: + 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.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + fn.name@1.1.0: {} + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fractional-indexing@3.2.0: {} + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + fuzzy@0.1.3: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + 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 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glur@1.1.2: {} + + gopd@1.2.0: {} + + hachure-fill@0.5.2: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hono@4.11.10: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + image-blob-reduce@3.0.1: + dependencies: + pica: 7.1.1 + + immutable@4.3.7: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + ip-address@10.0.1: {} + + ipaddr.js@1.9.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-promise@4.0.0: {} + + is-stream@2.0.1: {} + + isexe@2.0.0: {} + + jose@6.1.3: {} + + jotai-scope@0.7.2(jotai@2.11.0(@types/react@18.3.28)(react@18.3.1))(react@18.3.1): + dependencies: + jotai: 2.11.0(@types/react@18.3.28)(react@18.3.1) + react: 18.3.1 + + jotai@2.11.0(@types/react@18.3.28)(react@18.3.1): + optionalDependencies: + '@types/react': 18.3.28 + react: 18.3.1 + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json5@2.2.3: {} + + katex@0.16.28: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + + kleur@4.1.5: {} + + kuler@2.0.0: {} + + langium@4.2.1: + dependencies: + chevrotain: 11.1.1 + chevrotain-allstar: 0.3.1(chevrotain@11.1.1) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.1.0 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + lodash-es@4.17.23: {} + + lodash.debounce@4.0.8: {} + + lodash.throttle@4.1.1: {} + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + marked@16.4.2: {} + + math-intrinsics@1.1.0: {} + + mdast-util-from-markdown@1.3.1: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + decode-named-character-reference: 1.3.0 + mdast-util-to-string: 3.2.0 + micromark: 3.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-decode-string: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-stringify-position: 3.0.3 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + mdast-util-to-string@3.2.0: + dependencies: + '@types/mdast': 3.0.15 + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@1.0.3: {} + + merge-descriptors@2.0.0: {} + + mermaid@10.9.3: + dependencies: + '@braintree/sanitize-url': 6.0.2 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.10 + dayjs: 1.11.19 + dompurify: 3.1.6 + elkjs: 0.9.3 + katex: 0.16.28 + khroma: 2.1.0 + lodash-es: 4.17.23 + mdast-util-from-markdown: 1.3.1 + non-layered-tidy-tree-layout: 2.0.2 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 9.0.1 + web-worker: 1.5.0 + transitivePeerDependencies: + - supports-color + + mermaid@10.9.4: + dependencies: + '@braintree/sanitize-url': 6.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.10 + dayjs: 1.11.19 + dompurify: 3.1.6 + elkjs: 0.9.3 + katex: 0.16.28 + khroma: 2.1.0 + lodash-es: 4.17.23 + mdast-util-from-markdown: 1.3.1 + non-layered-tidy-tree-layout: 2.0.2 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 9.0.1 + web-worker: 1.5.0 + transitivePeerDependencies: + - supports-color + + mermaid@11.12.3: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.0 + '@mermaid-js/parser': 1.0.0 + '@types/d3': 7.4.3 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.13 + dayjs: 1.11.19 + dompurify: 3.3.1 + katex: 0.16.28 + khroma: 2.1.0 + lodash-es: 4.17.23 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 11.1.0 + + methods@1.1.2: {} + + micromark-core-commonmark@1.1.0: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-factory-destination: 1.1.0 + micromark-factory-label: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-factory-title: 1.1.0 + micromark-factory-whitespace: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-html-tag-name: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-factory-destination@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-label@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-factory-space@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + + micromark-factory-title@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-whitespace@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-character@1.2.0: + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-chunked@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-classify-character@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-combine-extensions@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-decode-numeric-character-reference@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-decode-string@1.1.0: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 1.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-symbol: 1.1.0 + + micromark-util-encode@1.1.0: {} + + micromark-util-html-tag-name@1.2.0: {} + + micromark-util-normalize-identifier@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-resolve-all@1.1.0: + dependencies: + micromark-util-types: 1.1.0 + + micromark-util-sanitize-uri@1.2.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-encode: 1.1.0 + micromark-util-symbol: 1.1.0 + + micromark-util-subtokenize@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-util-symbol@1.1.0: {} + + micromark-util-types@1.1.0: {} + + micromark@3.2.0: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-combine-extensions: 1.1.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-encode: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + mimic-response@3.1.0: {} + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + mri@1.2.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + multimath@2.0.0: + dependencies: + glur: 1.1.2 + object-assign: 4.1.1 + + nanoid@3.3.11: {} + + nanoid@3.3.3: {} + + nanoid@4.0.2: {} + + napi-build-utils@2.0.0: {} + + negotiator@0.6.3: {} + + negotiator@1.0.0: {} + + node-abi@3.87.0: + dependencies: + semver: 7.7.4 + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-releases@2.0.27: {} + + non-layered-tidy-tree-layout@2.0.2: {} + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + open-color@1.9.1: {} + + package-manager-detector@1.6.0: {} + + pako@2.0.3: {} + + parseurl@1.3.3: {} + + path-data-parser@0.1.0: {} + + path-key@3.1.1: {} + + path-to-regexp@0.1.12: {} + + path-to-regexp@8.3.0: {} + + pathe@2.0.3: {} + + perfect-freehand@1.2.0: {} + + pica@7.1.1: + dependencies: + glur: 1.1.2 + inherits: 2.0.4 + multimath: 2.0.0 + object-assign: 4.1.1 + webworkify: 1.5.0 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pkce-challenge@5.0.1: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + png-chunk-text@1.0.0: {} + + png-chunks-encode@1.0.0: + dependencies: + crc-32: 0.3.0 + sliced: 1.0.1 + + png-chunks-extract@1.0.0: + dependencies: + crc-32: 0.3.0 + + points-on-curve@0.2.0: {} + + points-on-curve@1.0.1: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.87.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pwacompat@2.0.17: {} + + qs@6.14.2: + dependencies: + side-channel: 1.1.0 + + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@16.13.1: {} + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.28)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + react-remove-scroll@2.7.2(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.28)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.28)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.28)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.28)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + + react-split@2.0.14(react@18.3.1): + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + split.js: 1.6.5 + + react-style-singleton@2.2.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + robust-predicates@3.0.2: {} + + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + roughjs@4.6.4: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + rw@1.3.3: {} + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sass@1.51.0: + dependencies: + chokidar: 3.6.0 + immutable: 4.3.7 + source-map-js: 1.2.1 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + semver@7.7.4: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + sliced@1.0.1: {} + + source-map-js@1.2.1: {} + + split.js@1.6.5: {} + + stack-trace@0.0.10: {} + + statuses@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@2.0.1: {} + + stylis@4.3.6: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + text-hex@1.0.0: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tree-kill@1.2.2: {} + + triple-beam@1.4.1: {} + + ts-dedent@2.2.0: {} + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tunnel-rat@0.1.2(@types/react@18.3.28)(react@18.3.1): + dependencies: + zustand: 4.5.7(@types/react@18.3.28)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - immer + - react + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + undici-types@6.21.0: {} + + unist-util-stringify-position@3.0.3: + dependencies: + '@types/unist': 2.0.11 + + unpipe@1.0.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-callback-ref@1.3.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + use-sidecar@1.1.3(@types/react@18.3.28)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.28 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@11.1.0: {} + + uuid@9.0.1: {} + + uvu@0.5.6: + dependencies: + dequal: 2.0.3 + diff: 5.2.2 + kleur: 4.1.5 + sade: 1.8.1 + + vary@1.1.2: {} + + vite@6.4.1(@types/node@20.19.33)(sass@1.51.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.33 + fsevents: 2.3.3 + sass: 1.51.0 + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.1.0: {} + + web-streams-polyfill@3.3.3: {} + + web-worker@1.5.0: {} + + webworkify@1.5.0: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.19.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.8 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.19.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} + + zustand@4.5.7(@types/react@18.3.28)(react@18.3.1): + dependencies: + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + react: 18.3.1 diff --git a/skills/excalidraw-skill/SKILL.md b/skills/excalidraw-skill/SKILL.md index 2400053..77710dc 100644 --- a/skills/excalidraw-skill/SKILL.md +++ b/skills/excalidraw-skill/SKILL.md @@ -1,280 +1,370 @@ --- name: excalidraw-skill -description: Programmatic canvas toolkit for creating, editing, and refining Excalidraw diagrams via MCP tools with real-time canvas sync. Use when an agent needs to (1) draw or lay out diagrams on a live canvas, (2) iteratively refine diagrams using describe_scene and get_canvas_screenshot to see its own work, (3) export/import .excalidraw files or PNG/SVG images, (4) save/restore canvas snapshots, (5) convert Mermaid to Excalidraw, or (6) perform element-level CRUD, alignment, distribution, grouping, duplication, and locking. Requires a running canvas server (EXPRESS_SERVER_URL, default http://localhost:3000). +description: Programmatic canvas toolkit for creating, editing, and refining Excalidraw diagrams via MCP tools (32 tools) or REST API with real-time canvas sync, multi-tenant workspace isolation, SQLite persistence, project management, full-text search, and element version history. Use when an agent needs to draw or lay out diagrams on a live canvas, iteratively refine diagrams using screenshots, manage workspaces/tenants and projects, export/import .excalidraw files or PNG/SVG images, search elements, view change history, save/restore canvas snapshots, or perform element-level CRUD. Canvas server port is configurable via CANVAS_PORT env var (default 3000). --- # Excalidraw Skill ## Step 0: Detect Connection Mode -Before doing anything, determine which mode is available. Run these checks **in order**: +Run these checks **in order**: + +1. **MCP Server** (best): If tools like `batch_create_elements` are available → use MCP mode. +2. **REST API** (fallback): `curl -s http://localhost:3000/health` returns `{"status":"ok"}` → use REST API mode. +3. **Nothing works**: Guide user to install (clone `sanjibdevnathlabs/mcp-excalidraw-local`, build, configure MCP). + +See `references/cheatsheet.md` for the full MCP-vs-REST mapping and REST API gotchas. + +## Core Principles (Read Before Any Diagram) + +These principles were learned through extensive iterative use. Violating them produces bad diagrams. + +### 1. Never Trust Blind Output — Use the Write-Check-Review Cycle + +Every diagram iteration follows this mandatory loop: -### Check 1: MCP Server (Best experience) -```bash -mcp-cli tools | grep excalidraw ``` -If you see tools like `excalidraw/batch_create_elements` → **use MCP mode**. Call MCP tools directly. - -### Check 2: REST API (Fallback — works without MCP server) -```bash -curl -s http://localhost:3000/health +WRITE (create/update elements) + → CHECK (screenshot to see actual rendering) + → REVIEW (critically evaluate against Quality Checklist) + → FIX (if issues found, fix and re-screenshot) + → only proceed when ALL checks pass ``` -If you get `{"status":"ok"}` → **use REST API mode**. Use HTTP endpoints (`curl` / `fetch`) from the cheatsheet. -### Check 3: Nothing works → Guide user to install -If neither works, tell the user: -> The Excalidraw canvas server is not running. To set up: -> 1. Clone: `git clone https://github.com/yctimlin/mcp_excalidraw && cd mcp_excalidraw` -> 2. Build: `npm ci && npm run build` -> 3. Start canvas: `HOST=0.0.0.0 PORT=3000 npm run canvas` -> 4. Open `http://localhost:3000` in a browser -> 5. (Recommended) Install the MCP server for the best experience: -> ``` -> claude mcp add excalidraw -s user -e EXPRESS_SERVER_URL=http://localhost:3000 -- node /path/to/mcp_excalidraw/dist/index.js -> ``` +**Screenshot strategy**: `get_canvas_screenshot` may return empty images. When it fails, use Chrome DevTools MCP (`take_screenshot` after `navigate_page` to canvas URL) as a reliable fallback. -### MCP vs REST API Quick Reference +### 2. Use batch_create_elements, Not Mermaid -| Operation | MCP Tool | REST API Equivalent | -|-----------|----------|-------------------| -| Create elements | `batch_create_elements` | `POST /api/elements/batch` with `{"elements": [...]}` | -| Get all elements | `query_elements` | `GET /api/elements` | -| Get one element | `get_element` | `GET /api/elements/:id` | -| Update element | `update_element` | `PUT /api/elements/:id` | -| Delete element | `delete_element` | `DELETE /api/elements/:id` | -| Clear canvas | `clear_canvas` | `DELETE /api/elements/clear` | -| Describe scene | `describe_scene` | `GET /api/elements` (parse manually) | -| Export scene | `export_scene` | `GET /api/elements` (save to file) | -| Import scene | `import_scene` | `POST /api/elements/sync` with `{"elements": [...]}` | -| Snapshot | `snapshot_scene` | `POST /api/snapshots` with `{"name": "..."}` | -| Restore snapshot | `restore_snapshot` | `GET /api/snapshots/:name` then `POST /api/elements/sync` | -| Screenshot | `get_canvas_screenshot` | Only via MCP (needs browser) | -| Design guide | `read_diagram_guide` | Not available — see cheatsheet for guidelines | -| Viewport | `set_viewport` | `POST /api/viewport` (needs browser) | -| Export image | `export_to_image` | `POST /api/export/image` (needs browser) | -| Export URL | `export_to_excalidraw_url` | Only via MCP | +The `create_from_mermaid` tool produces **low-quality output**: overlapping text, poor spacing, unreadable labels. It is a quick preview tool, not a production tool. -### REST API Gotchas (Critical — read before using REST API) +For quality diagrams, **always use `batch_create_elements`** with precise coordinates, explicit sizing, and color coding. The extra planning time pays for itself in fewer fix iterations. -1. **Labels**: Use `"label": {"text": "My Label"}` (not `"text": "My Label"`). MCP tools auto-convert, REST API does not. -2. **Arrow binding**: Use `"start": {"id": "svc-a"}, "end": {"id": "svc-b"}` (not `"startElementId"`/`"endElementId"`). MCP tools accept `startElementId` and convert, REST API requires the `start`/`end` object format directly. -3. **fontFamily**: Must be a string (e.g. `"1"`) or omit it entirely. Do NOT pass a number like `1`. -4. **Updating labels**: When updating a shape via `PUT /api/elements/:id`, include the full `label` in the update body to preserve it. Omitting `label` from the update won't delete it, but re-sending ensures it renders correctly. -5. **Screenshot in REST mode**: `POST /api/export/image` returns `{"data": ""}`. Save to file and read it back for visual verification. Requires browser open. +### 3. Shapes First, Arrows Second — Two Separate Batches -## Quality Gate (MANDATORY — read before creating any diagram) +Create shapes in one batch, then arrows in a separate batch. Arrow binding (`startElementId`/`endElementId`) requires shapes to already exist in the scene. Mixing both in one call can work but often produces binding errors. -**After EVERY iteration (each batch of elements added), you MUST run a quality check before proceeding. NEVER say "looks great" unless ALL checks pass.** +### 4. Multiple Diagrams on One Canvas -### Quality Checklist — verify ALL before adding more elements: -1. **Text truncation**: Is ALL text fully visible? Labels must fit inside their shapes. If text is cut off or wrapping badly → increase `width` and/or `height`. -2. **Overlap**: Do ANY elements overlap each other? Check that no rectangles, ellipses, or text elements share the same space. Background zones must fully contain their children with padding. -3. **Arrow crossing**: Do arrows cross through unrelated elements or overlap with text labels? If yes → **use curved/elbowed arrows with waypoints** to route around obstacles (see "Arrow Routing" section). Never accept crossing arrows. -4. **Arrow-text overlap**: Do any arrow labels ("charge", "event", etc.) overlap with shapes? Arrow labels are positioned at the midpoint — if they overlap, either remove the label, shorten it, or adjust the arrow path. -5. **Spacing**: Is there at least 40px gap between elements? Cramped layouts are unreadable. -6. **Readability**: Can all labels be read at normal zoom? Font size >= 16 for body text, >= 20 for titles. +**Never clear the canvas** between diagrams. Place them side-by-side or in a grid: -### If ANY issue is found: -- **STOP adding new elements** -- Fix the issue first (resize, reposition, delete and recreate) -- Re-verify with a new screenshot -- Only proceed to next iteration after ALL checks pass +``` +Diagram 1: x=0 to ~1100 +Diagram 2: x=1400 onward (300px gap) +— or — +Row 1: y=0 to ~800 +Row 2: y=1100 onward (300px gap) +``` -### Sizing Rules (prevent truncation): -- **Shape width**: `max(160, labelTextLength * 9)` pixels. For multi-word labels like "API Gateway (Kong)", count all characters. -- **Shape height**: 60px for single line, 80px for 2 lines, 100px for 3 lines. -- **Background zones**: Add 50px padding on ALL sides around contained elements. -- **Element spacing**: 60px vertical between tiers, 40px horizontal between siblings. -- **Side panels**: Place at least 80px away from main diagram elements. -- **Arrow labels**: Keep labels short (1-2 words). Long arrow labels overlap with other elements. +Use a title text element above each diagram to label it. -### Layout Planning (prevent overlap): -Before creating elements, **plan your coordinate grid** on paper first: -- Tier 1 (y=50-130): Client apps -- Tier 2 (y=200-280): Gateway/Edge -- Tier 3 (y=350-440): Services (spread wide: each service ~180px apart) -- Tier 4 (y=510-590): Data stores -- Side panels: x < 0 (left) or x > mainDiagramRight + 80 (right) +### 5. Set roughness: 0 for Clean Diagrams -**Do NOT place side panels (observability, external APIs) at the same x-range as the main diagram — they WILL overlap.** +Excalidraw defaults to hand-drawn style (roughness > 0). For professional, readable diagrams, always set `"roughness": 0` on every element. Also use `"strokeWidth": 2` for arrows to ensure visibility. -## Quick Start +## Sizing Rules (Critical — Prevents Truncation) -1. Run **Step 0** above to detect your connection mode. -2. Open the canvas URL in a browser (required for image export/screenshot). -3. **MCP mode**: Use MCP tools for all operations. **REST mode**: Use HTTP endpoints from cheatsheet. -4. For full tool/endpoint reference, read `references/cheatsheet.md`. +Excalidraw's Virgil font is ~30% wider than standard fonts. These rules account for that. + +### Rectangles + +``` +width: max(200, characterCount * 11) +height: 70 (1 line), 80 (2 lines), 100 (3 lines) +fontSize: 16-20 +``` + +### Diamonds (Decision Nodes) + +Diamond usable text area is ~50% of the bounding box. **Double your width estimate.** + +``` +width: max(400, longestLineChars * 18) +height: max(160, lineCount * 50) +fontSize: 16 +``` + +A diamond with text "Behavioral guideline\nor project standard?" (20 chars) needs at least 400x160. + +### Ellipses + +Ellipse text area is ~60% of bounding box. Size generously. + +``` +width: max(280, characterCount * 14) +height: max(65, lineCount * 35) +fontSize: 16-18 +``` + +### Text Elements (Standalone Titles) + +``` +fontSize: 24-28 for diagram titles +fontSize: 16-20 for annotations +``` + +## Arrow Visibility Rules (Critical — Prevents Invisible Arrows) + +When arrows are bound to shapes via `startElementId`/`endElementId`, the actual rendered arrow length equals the **gap between shape edges minus binding padding (8px each side)**. If shapes are too close, arrows shrink to 0px and become invisible. + +### Minimum Gap Between Connected Shapes + +| Connection Direction | Minimum Gap | Recommended Gap | +|---------------------|-------------|-----------------| +| Vertical (top-down flow) | 80px | 120px | +| Horizontal (left-right) | 100px | 140px | + +### Calculating Vertical Gap for Flowcharts + +``` +gap = nextShapeY - (currentShapeY + currentShapeHeight) + +Example (diamonds h=160, gap needed ≥ 120): + Q1: y=260, h=160 → bottom edge = 420 + Q2: y=540 → gap = 540 - 420 = 120px ✓ +``` + +If the gap is < 80px, arrows will be too short to see — especially with labels like "YES"/"NO". ## Workflow: Draw A Diagram -### MCP Mode -1. **Call `read_diagram_guide`** first to load design best practices. -2. **Plan your coordinate grid** (see Quality Gate → Layout Planning) before writing any JSON. -3. Optional: `clear_canvas` to start fresh. -4. Use `batch_create_elements` with shapes AND arrows in one call. -5. **Assign custom `id` to shapes** (e.g. `"id": "auth-svc"`). Set `text` field to label shapes. -6. **Size shapes for their text** — use `width: max(160, textLength * 9)`. -7. **Bind arrows** using `startElementId` / `endElementId` — arrows auto-route. -8. `set_viewport` with `scrollToContent: true` to auto-fit the diagram. -9. **Run Quality Checklist** — `get_canvas_screenshot` and critically evaluate. Fix issues before proceeding. +### Phase 1: Plan -### REST API Mode -1. Read `references/cheatsheet.md` for design guidelines. -2. **Plan your coordinate grid** (see Quality Gate → Layout Planning) before writing any JSON. -3. Optional: `curl -X DELETE http://localhost:3000/api/elements/clear` -4. Create elements in one call (use `@file.json` for large payloads): - ```bash - curl -X POST http://localhost:3000/api/elements/batch \ - -H "Content-Type: application/json" \ - -d '{"elements": [ - {"id": "svc-a", "type": "rectangle", "x": 0, "y": 0, "width": 160, "height": 60, "label": {"text": "Service A"}}, - {"id": "svc-b", "type": "rectangle", "x": 0, "y": 200, "width": 160, "height": 60, "label": {"text": "Service B"}}, - {"type": "arrow", "x": 0, "y": 0, "start": {"id": "svc-a"}, "end": {"id": "svc-b"}} - ]}' - ``` -5. **Use `"label": {"text": "..."}` for shape labels** (not `"text": "..."`). -6. **Bind arrows with `"start": {"id": "..."}` / `"end": {"id": "..."}`** — server auto-routes edges. -7. **Size shapes for their text** — use `width: max(160, labelTextLength * 9)`. -8. **Run Quality Checklist** — take screenshot, critically evaluate. Fix issues before adding more elements. +Before writing any JSON, plan on paper: -### Arrow Binding (Recommended) +1. **List all elements**: shapes, labels, connections +2. **Choose layout direction**: top-down (flowcharts), left-right (timelines), grid (architecture) +3. **Assign coordinates**: use the sizing rules above to compute widths/heights, then lay out with proper gaps +4. **Assign IDs**: every shape needs a custom `id` so arrows can reference it -Bind arrows to shapes for auto-routed edges. The format differs between MCP and REST API: +### Phase 2: Create Shapes (Batch 1) -**MCP Mode** — use `startElementId` / `endElementId`: ```json {"elements": [ - {"id": "svc-a", "type": "rectangle", "x": 0, "y": 0, "width": 120, "height": 60, "text": "Service A"}, - {"id": "svc-b", "type": "rectangle", "x": 0, "y": 200, "width": 120, "height": 60, "text": "Service B"}, - {"type": "arrow", "x": 0, "y": 0, "startElementId": "svc-a", "endElementId": "svc-b", "text": "calls"} + {"id": "title", "type": "text", "x": 100, "y": 0, + "text": "MY DIAGRAM", "fontSize": 28, "strokeColor": "#1e1e1e"}, + {"id": "box-a", "type": "rectangle", "x": 0, "y": 80, + "width": 200, "height": 70, "text": "Service A", + "backgroundColor": "#a5d8ff", "strokeColor": "#1971c2", + "roughness": 0, "fontSize": 18}, + {"id": "box-b", "type": "rectangle", "x": 0, "y": 280, + "width": 200, "height": 70, "text": "Service B", + "backgroundColor": "#b2f2bb", "strokeColor": "#2f9e44", + "roughness": 0, "fontSize": 18} ]} ``` -**REST API Mode** — use `start: {id}` / `end: {id}` and `label: {text}`: +### Phase 3: Create Arrows (Batch 2) + ```json {"elements": [ - {"id": "svc-a", "type": "rectangle", "x": 0, "y": 0, "width": 120, "height": 60, "label": {"text": "Service A"}}, - {"id": "svc-b", "type": "rectangle", "x": 0, "y": 200, "width": 120, "height": 60, "label": {"text": "Service B"}}, - {"type": "arrow", "x": 0, "y": 0, "start": {"id": "svc-a"}, "end": {"id": "svc-b"}, "label": {"text": "calls"}} + {"type": "arrow", "x": 100, "y": 150, + "startElementId": "box-a", "endElementId": "box-b", + "width": 0, "height": 130, "text": "calls", + "strokeColor": "#1e1e1e", "roughness": 0, "strokeWidth": 2, + "endArrowhead": "arrow"} ]} ``` -Arrows without binding use manual `x`, `y`, `points` coordinates. +### Phase 4: Check (MANDATORY) -### Arrow Routing — Avoid Overlaps (Critical for complex diagrams) +1. `set_viewport` with `scrollToContent: true` +2. Wait 1-2 seconds for render +3. Take screenshot (MCP `get_canvas_screenshot` or Chrome DevTools `take_screenshot`) +4. **Critically evaluate** against the Quality Checklist below +5. Fix any issues, re-screenshot, repeat until clean -Straight arrows (2-point) cause crossing and overlap in complex diagrams. **Use curved or elbowed arrows instead:** +## Quality Checklist + +After EVERY batch of elements, verify ALL of these: + +| Check | What to Look For | Fix | +|-------|-----------------|-----| +| **Text truncation** | Any label cut off or hidden? | Increase shape width/height | +| **Invisible arrows** | Can you see arrows between all connected shapes? | Increase gap between shapes to ≥ 120px | +| **Arrow labels** | Do YES/NO/labels overlap with shapes? | Shorten labels or increase gap | +| **Overlap** | Do any elements share space? | Reposition with more spacing | +| **Readability** | Can all text be read at 50-70% zoom? | Increase fontSize to ≥ 16 | +| **Spacing** | At least 40px gap between unconnected elements? | Spread elements apart | + +### If ANY Check Fails + +**STOP.** Do not add more elements. Fix the issue first: + +1. Use `update_element` to resize/reposition +2. Or `delete_element` + recreate with better coordinates +3. Re-screenshot to verify the fix +4. Only proceed when ALL checks pass + +### How to Honestly Evaluate a Screenshot + +- Zoom into different regions — don't just glance at the overview +- Check every label individually for truncation +- Trace every arrow path for visibility +- **If you see ANY issue, say "I see [issue], fixing it"** — never say "looks great" unless it truly is + +## Color Palette + +Use consistent colors from this palette: + +| Role | Fill | Stroke | Use For | +|------|------|--------|---------| +| Primary | #a5d8ff | #1971c2 | Main flow, services | +| Success | #b2f2bb | #2f9e44 | Approved, healthy, YES paths | +| Warning | #ffd8a8 | #e8590c | Attention, agents | +| Error | #ffc9c9 | #e03131 | Critical, NO paths, failures | +| Purple | #eebefa | #9c36b5 | Rules, governance | +| Cyan | #99e9f2 | #0c8599 | Data stores, MCP | +| Neutral | #e9ecef | #868e96 | Secondary, annotations | +| Default | #ffffff | #1e1e1e | Decisions, generic | + +## Flowchart Template (Tested & Verified) + +This template produces clean, readable decision flowcharts: -**Option 1: Curved arrows** — add intermediate waypoints + `roundness`: -```json -{ - "type": "arrow", "x": 100, "y": 100, - "points": [[0, 0], [50, -40], [200, 0]], - "roundness": {"type": 2}, - "strokeColor": "#1971c2" -} ``` -The waypoint `[50, -40]` pushes the arrow upward to arc over elements. `roundness: {type: 2}` makes it a smooth curve. - -**Option 2: Elbowed arrows** — right-angle routing (L-shaped or Z-shaped): -```json -{ - "type": "arrow", "x": 100, "y": 100, - "points": [[0, 0], [0, -50], [200, -50], [200, 0]], - "elbowed": true, - "strokeColor": "#1971c2" -} +Layout: + Diamonds: w=400, h=160, fontSize=16, gap=120px vertical + Answer boxes: w=300, h=80, fontSize=20, offset 130px right of diamonds + Start ellipse: w=340, h=70, fontSize=18 + Title: fontSize=28 + Arrows: strokeWidth=2, roughness=0 + YES arrows: strokeColor=#2f9e44 (green), horizontal right + NO arrows: strokeColor=#e03131 (red), vertical down + All elements: roughness=0 ``` -**When to use which:** -- **Fan-out arrows** (one source → many targets): Use curved arrows with waypoints spread vertically to avoid overlapping each other. -- **Cross-lane arrows** (connecting to side panels): Use elbowed arrows that route around the main diagram — go UP first, then ACROSS, then DOWN. -- **Inter-service arrows** (horizontal connections): Use curved arrows with a slight vertical offset to avoid crossing through adjacent elements. +## Architecture Diagram Template -**Rule of thumb:** If an arrow would cross through an unrelated element, add a waypoint to route around it. Never accept crossing arrows — always fix them. - -## Workflow: Iterative Refinement (Key Differentiator) - -The feedback loop that makes this skill unique. **Each iteration MUST include a quality check.** - -### MCP Mode (full feedback loop) -1. Add elements (`batch_create_elements`, `create_element`). -2. `set_viewport` with `scrollToContent: true`. -3. `get_canvas_screenshot` — **critically evaluate** against the Quality Checklist. -4. **If issues found** → fix them (`update_element`, `delete_element`, resize, reposition). -5. `get_canvas_screenshot` again — re-verify fix. -6. **Only proceed to next iteration when ALL quality checks pass.** - -### REST API Mode (partial feedback loop) -1. Add elements via `POST /api/elements/batch`. -2. `POST /api/viewport` with `{"scrollToContent": true}`. -3. Take screenshot: `POST /api/export/image` → save PNG → **critically evaluate** against Quality Checklist. -4. **If issues found** → fix via `PUT /api/elements/:id` or delete and recreate. -5. Re-screenshot and re-verify. -6. **Only proceed to next iteration when ALL quality checks pass.** - -### How to critically evaluate a screenshot: -- Look at EVERY label — is any text cut off or overflowing its container? -- Look at EVERY arrow — does any arrow pass through an unrelated element? -- Look at ALL element pairs — do any overlap or touch? -- Look at spacing — is anything crammed together? -- **Be honest.** If you see ANY issue, say "I see [issue], fixing it" — not "looks great". - -Example flow (MCP): ``` -batch_create_elements → get_canvas_screenshot → "text truncated on 2 shapes" -→ update_element (increase widths) → get_canvas_screenshot → "overlap between X and Y" -→ update_element (reposition) → get_canvas_screenshot → "all checks pass" -→ proceed to next iteration +Layout: + Zones: large rectangles, backgroundColor=#e9ecef, opacity=30 + Services: w=200, h=70, fontSize=18, spaced 60px apart + Data stores: w=180, h=60, fontSize=16, strokeColor=#0c8599 + Arrows: solid for sync, dashed (strokeStyle="dashed") for async + Title: fontSize=24 above each zone ``` +## Workflow: Iterative Refinement + +``` +create shapes (batch 1) + → create arrows (batch 2) + → set_viewport(scrollToContent: true) + → wait 1-2s + → screenshot + → evaluate quality checklist + → issues? fix → re-screenshot → re-evaluate + → clean? proceed to next diagram section +``` + +For multi-diagram canvases, offset each new diagram by 300px+ from the previous one's bounding box. + +## Workflow: Multi-Tenancy (Workspaces) + +The MCP is multi-tenant. Each Cursor workspace automatically gets its own tenant (identified by a SHA-256 hash of the workspace path). All elements, projects, and snapshots are scoped to the active tenant. + +### Automatic Tenant Detection + +On MCP startup, the server: +1. Creates a tenant from `process.cwd()` (initial guess) +2. After connecting, calls `server.listRoots()` to get the real workspace path from Cursor +3. If different, re-creates/switches to the correct tenant and notifies the canvas + +This means globally-configured MCPs (`~/.cursor/mcp.json`) correctly detect the per-window workspace — no manual setup needed. + +### Tenant Operations + +| Task | Tool | Notes | +|------|------|-------| +| See all workspaces | `list_tenants` | Returns id, name, workspace_path, created_at | +| Switch workspace | `switch_tenant` with `tenantId` | Canvas reloads that tenant's elements via WebSocket | +| Check current tenant | (from describe_scene or frontend header) | Shows "Workspace: [name]" in canvas | + +### Multiple Cursor Instances + +Each instance sends its own `X-Tenant-Id` header on every HTTP/MCP request. SQLite uses `busy_timeout` for concurrent write safety. No state conflicts between windows. + +## Workflow: Projects (Within a Tenant) + +Projects group diagrams within a tenant. Each tenant has a "Default Project" created automatically. Use projects to organize different diagram sets (e.g., "Architecture", "User Flows", "Sprint Planning"). + +| Task | Tool | Notes | +|------|------|-------| +| List projects | `list_projects` | Shows all projects in active tenant | +| Switch project | `switch_project` with `projectId` | Elements change to that project's set | +| Create new project | `switch_project` with `createName` | Creates and switches in one call | + +## Workflow: Search & History + +### Full-Text Search + +`search_elements` with `query` — searches across element labels and text content in the active project. Useful for finding specific elements in large diagrams. + +### Element Version History + +`element_history` — view create/update/delete operations for: +- A specific element: pass `elementId` +- Entire active project: omit `elementId` +- Control result count with `limit` (default 50) + +Use history to debug unexpected changes or audit what was modified. + ## Workflow: Refine An Existing Diagram -1. `describe_scene` to understand current state. -2. Identify targets by id, type, or label text (not x/y coordinates). -3. `update_element` to move/resize/recolor, `delete_element` to remove. -4. `get_canvas_screenshot` to verify changes visually. -5. If updates fail: check element id exists (`get_element`), element isn't locked (`unlock_elements`). +1. `describe_scene` to understand current state +2. Identify targets by `id` or label text (or use `search_elements` for text search) +3. `update_element` to move/resize/recolor +4. Screenshot to verify +5. If updates fail: check element id exists (`get_element`), element isn't locked +6. Use `element_history` to see what changed if something looks wrong -## Workflow: File I/O (Diagrams-as-Code) +## Workflow: File I/O -- Export to .excalidraw format: `export_scene` with optional `filePath`. -- Import from .excalidraw: `import_scene` with `mode: "replace"` or `"merge"`. -- Export to image: `export_to_image` with `format: "png"` or `"svg"` (requires browser open). -- CLI export: `node scripts/export-elements.cjs --out diagram.elements.json` -- CLI import: `node scripts/import-elements.cjs --in diagram.elements.json --mode batch|sync` +- Export: `export_scene` (optional `filePath`) +- Import: `import_scene` with `mode: "replace"` or `"merge"` +- Image export: `export_to_image` with `format: "png"` or `"svg"` (requires browser) -## Workflow: Snapshots (Save/Restore Canvas State) +## Workflow: Snapshots -1. `snapshot_scene` with a name before risky changes. -2. Make changes, `describe_scene` / `get_canvas_screenshot` to evaluate. -3. `restore_snapshot` to rollback if needed. +1. `snapshot_scene` with a name before risky changes +2. Make changes, screenshot to evaluate +3. `restore_snapshot` to rollback if needed -## Workflow: Duplication - -- `duplicate_elements` with `elementIds` and optional `offsetX`/`offsetY` (default 20,20). -- Useful for creating repeated patterns or copying existing layouts. - -## Points Format for Arrows/Lines - -The `points` field accepts both formats: -- Tuple: `[[0, 0], [100, 50]]` -- Object: `[{"x": 0, "y": 0}, {"x": 100, "y": 50}]` - -Both are normalized to tuples automatically. - -## Workflow: Share Diagram (excalidraw.com URL) - -1. Create your diagram using any of the above workflows. -2. `export_to_excalidraw_url` — uploads encrypted scene, returns a shareable URL. -3. Share the URL — anyone can open it in excalidraw.com to view and edit. +**Note**: Snapshot restore may not always reload elements into the active view. If the canvas appears empty after restore, re-fetch elements or recreate. ## Workflow: Viewport Control -- `set_viewport` with `scrollToContent: true` — auto-fit all elements (zoom-to-fit). -- `set_viewport` with `scrollToElementId: "my-element"` — center view on a specific element. -- `set_viewport` with `zoom: 1.5, offsetX: 100, offsetY: 200` — manual camera control. +- `scrollToContent: true` — auto-fit all elements +- `scrollToElementId: "my-element"` — center on specific element +- `zoom: 0.7, offsetX: 100, offsetY: 50` — manual camera for close-up review + +## Anti-Patterns (Common Mistakes) + +| Mistake | Why It Fails | Do This Instead | +|---------|-------------|-----------------| +| Using `create_from_mermaid` for final diagrams | Overlapping text, poor layout | Use `batch_create_elements` with coordinates | +| Shapes too small for text | Truncation, especially in diamonds | Use sizing formulas above | +| No gap between connected shapes | Arrows become invisible (0px length) | Maintain 120px+ vertical gap | +| Clearing canvas between diagrams | Loses previous work | Place diagrams side-by-side | +| Skipping screenshot verification | Invisible defects compound | Screenshot after EVERY batch | +| Shapes + arrows in one batch | Binding errors | Shapes first, arrows second | +| Default roughness (hand-drawn look) | Unprofessional for technical diagrams | Set `roughness: 0` on all elements | +| Trusting MCP screenshot alone | May return empty image | Use Chrome DevTools as fallback | + +## MCP Tool Quick Reference (32 Tools) + +| Category | Tools | +|----------|-------| +| Element CRUD (9) | `create_element`, `get_element`, `update_element`, `delete_element`, `query_elements`, `batch_create_elements`, `duplicate_elements`, `search_elements`, `element_history` | +| Layout (6) | `align_elements`, `distribute_elements`, `group_elements`, `ungroup_elements`, `lock_elements`, `unlock_elements` | +| Scene (4) | `describe_scene`, `get_canvas_screenshot`, `get_resource`, `read_diagram_guide` | +| File I/O (4) | `export_scene`, `import_scene`, `export_to_image`, `export_to_excalidraw_url` | +| State (3) | `clear_canvas`, `snapshot_scene`, `restore_snapshot` | +| Viewport (1) | `set_viewport` | +| Tenants (2) | `list_tenants`, `switch_tenant` | +| Projects (2) | `list_projects`, `switch_project` | +| Conversion (1) | `create_from_mermaid` (⚠ low quality — use `batch_create_elements` instead) | ## References -- `references/cheatsheet.md`: Complete MCP tool list (26 tools) + REST API endpoints + payload shapes. +- `references/cheatsheet.md`: Complete MCP tool list (32 tools) + REST API endpoints + payload shapes + env vars diff --git a/skills/excalidraw-skill/references/cheatsheet.md b/skills/excalidraw-skill/references/cheatsheet.md index fffa913..9e6286d 100644 --- a/skills/excalidraw-skill/references/cheatsheet.md +++ b/skills/excalidraw-skill/references/cheatsheet.md @@ -2,10 +2,12 @@ ## Defaults -- Canvas base URL: `EXPRESS_SERVER_URL` (default `http://localhost:3000`) +- Canvas base URL: configurable via `CANVAS_PORT` env var (default `3000`), resolves to `http://localhost:` - Canvas health: `GET /health` +- Data persistence: SQLite database at `~/.excalidraw-mcp/excalidraw.db` +- Multi-tenancy: each Cursor workspace auto-creates a tenant (hash of workspace path) -## MCP Tools (26 total) +## MCP Tools (32 total) ### Element CRUD @@ -15,9 +17,11 @@ | `get_element` | Get single element by ID | `id` | | `update_element` | Update element properties | `id` | | `delete_element` | Delete element | `id` | -| `query_elements` | Query by type/filters | (optional) `type`, `filter` | -| `batch_create_elements` | Create many at once | `elements[]` | +| `query_elements` | Query by type | (optional) `type` | +| `batch_create_elements` | Create many at once (recommended) | `elements[]` | | `duplicate_elements` | Clone with offset | `elementIds[]`, (optional) `offsetX`, `offsetY` | +| `search_elements` | Full-text search over labels/text | `query` | +| `element_history` | View version history (create/update/delete ops) | (optional) `elementId`, `limit` (default 50) | ### Layout & Organization @@ -35,8 +39,9 @@ | Tool | Description | Required params | |------|-------------|-----------------| | `describe_scene` | AI-readable scene description (types, positions, labels, connections, bounding box) | (none) | -| `get_canvas_screenshot` | Returns PNG image of canvas for visual verification | (optional) `background` | +| `get_canvas_screenshot` | Returns PNG image of canvas for visual verification (may return empty — use Chrome DevTools as fallback) | (optional) `background` | | `get_resource` | Get scene/library/theme/elements | `resource` | +| `read_diagram_guide` | Get design best practices (colors, sizing, layout, anti-patterns) | (none) | ### File I/O & Export @@ -45,15 +50,15 @@ | `export_scene` | Export to .excalidraw JSON | (optional) `filePath` | | `import_scene` | Import from .excalidraw JSON | `mode` ("replace"\|"merge"), `filePath` or `data` | | `export_to_image` | Export to PNG/SVG (needs browser) | `format` ("png"\|"svg"), (optional) `filePath`, `background` | -| `export_to_excalidraw_url` | Upload & get shareable excalidraw.com URL | (none) | +| `export_to_excalidraw_url` | Upload & get shareable excalidraw.com URL (may fail if org blocks excalidraw.com) | (none) | ### State Management | Tool | Description | Required params | |------|-------------|-----------------| -| `clear_canvas` | Remove all elements | (none) | -| `snapshot_scene` | Save named snapshot | `name` | -| `restore_snapshot` | Restore from snapshot | `name` | +| `clear_canvas` | Remove all elements from active project | (none) | +| `snapshot_scene` | Save named snapshot of current canvas state | `name` | +| `restore_snapshot` | Restore from snapshot (may not reload into view — re-fetch if canvas appears empty) | `name` | ### Viewport & Camera @@ -61,33 +66,65 @@ |------|-------------|-----------------| | `set_viewport` | Control camera: zoom-to-fit, center on element, manual zoom/scroll (needs browser) | (optional) `scrollToContent`, `scrollToElementId`, `zoom`, `offsetX`, `offsetY` | -### Design Guide +### Multi-Tenancy (Workspaces) | Tool | Description | Required params | |------|-------------|-----------------| -| `read_diagram_guide` | Get design best practices (colors, sizing, layout, anti-patterns) | (none) | +| `list_tenants` | List all tenants (workspaces). Each tenant maps to a Cursor workspace. | (none) | +| `switch_tenant` | Switch active tenant. All later operations use that tenant's projects/elements. | `tenantId` | + +### Projects (Within a Tenant) + +| Tool | Description | Required params | +|------|-------------|-----------------| +| `list_projects` | List all diagram projects in the active tenant | (none) | +| `switch_project` | Switch active project or create a new one | (optional) `projectId`, `createName`, `createDescription` | ### Conversion | Tool | Description | Required params | |------|-------------|-----------------| -| `create_from_mermaid` | Mermaid diagram to Excalidraw | `mermaidDiagram` | +| `create_from_mermaid` | Mermaid diagram to Excalidraw (⚠ produces low-quality output — use `batch_create_elements` for production diagrams) | `mermaidDiagram` | -Notes: -- **MCP tools**: Set `text` field on shapes to label them (auto-converts to `label.text`). Use `startElementId`/`endElementId` on arrows. -- **REST API**: Use `"label": {"text": "..."}` for shape labels. Use `"start": {"id": "..."}` / `"end": {"id": "..."}` for arrow binding. (Different format from MCP!) -- `fontFamily` must be a string (e.g. `"1"`) or omit it entirely — do NOT pass a number. -- `points` accepts both `[[x,y]]` tuples and `[{x,y}]` objects. -- **Curved arrows**: Use `"roundness": {"type": 2}` with 3+ points for smooth curves. Use `"elbowed": true` for right-angle routing. -- Prefer creating shapes first, then arrows, then alignment/grouping. +## Key Notes + +### MCP vs REST API Format Differences + +| Concept | MCP Tool Format | REST API Format | +|---------|----------------|-----------------| +| Shape labels | `"text": "My Label"` (auto-converts) | `"label": {"text": "My Label"}` | +| Arrow binding | `"startElementId": "id"` / `"endElementId": "id"` | `"start": {"id": "id"}` / `"end": {"id": "id"}` | +| `fontFamily` | String `"1"` or omit | String `"1"` or omit (never a number) | +| Tenant scoping | Auto (uses active tenant) | Include `X-Tenant-Id` header on every request | + +### Element Creation Best Practices + +- **Always set `roughness: 0`** for clean, professional diagrams (default is hand-drawn). +- **Always set `strokeWidth: 2`** on arrows for visibility. +- **Create shapes first, arrows second** (two separate `batch_create_elements` calls). +- **Assign custom `id`** to every shape so arrows can reference it. +- **Size shapes for their text** — Virgil font is ~30% wider than standard. Use sizing formulas from SKILL.md. +- `points` accepts both `[[x,y]]` tuples and `[{x,y}]` objects — normalized automatically. +- **Curved arrows**: Use `"roundness": {"type": 2}` with 3+ points. **Elbowed arrows**: Use `"elbowed": true`. + +### Multi-Tenancy Architecture + +- **Tenant**: Maps to a Cursor workspace. Auto-created on MCP startup from workspace path hash. +- **Project**: Groups diagrams within a tenant. Default project created per tenant. +- **Elements**: Belong to the active project within the active tenant. +- **Hierarchy**: Tenant → Project → Elements +- **Concurrent instances**: Multiple Cursor windows each send `X-Tenant-Id` header for isolation. SQLite `busy_timeout` handles concurrent writes. +- **Frontend workspace switcher**: Dropdown in the canvas UI header labeled "Workspace: [name] ▾" with search filter. ## Canvas REST API (HTTP) +All endpoints accept an optional `X-Tenant-Id` header to scope operations to a specific tenant. + ### Elements | Method | Endpoint | Description | |--------|----------|-------------| -| `GET` | `/api/elements` | List all elements | +| `GET` | `/api/elements` | List all elements in active project | | `GET` | `/api/elements/:id` | Get element by ID | | `POST` | `/api/elements` | Create element | | `PUT` | `/api/elements/:id` | Update element | @@ -95,29 +132,37 @@ Notes: | `DELETE` | `/api/elements/clear` | Clear all elements | | `GET` | `/api/elements/search?type=...` | Search with filters | | `POST` | `/api/elements/batch` | Batch create | -| `POST` | `/api/elements/sync` | Overwrite import (clear + write) | +| `POST` | `/api/elements/sync` | Full sync (clear + write all elements) | | `POST` | `/api/elements/from-mermaid` | Mermaid conversion via frontend | +### Tenants + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/tenants` | List all tenants | +| `GET` | `/api/tenant/active` | Get active tenant | +| `PUT` | `/api/tenant/active` | Switch active tenant `{"tenantId": "..."}` (broadcasts to all WebSocket clients) | + ### Export | Method | Endpoint | Description | |--------|----------|-------------| -| `POST` | `/api/export/image` | Request image export (needs frontend) | +| `POST` | `/api/export/image` | Request image export (needs browser) | | `POST` | `/api/export/image/result` | Frontend posts export result back | ### Viewport | Method | Endpoint | Description | |--------|----------|-------------| -| `POST` | `/api/viewport` | Set viewport/camera (needs frontend) | +| `POST` | `/api/viewport` | Set viewport/camera (needs browser) | | `POST` | `/api/viewport/result` | Frontend posts viewport result back | ### Snapshots | Method | Endpoint | Description | |--------|----------|-------------| -| `POST` | `/api/snapshots` | Save snapshot `{name}` | -| `GET` | `/api/snapshots` | List snapshots | +| `POST` | `/api/snapshots` | Save snapshot `{"name": "..."}` | +| `GET` | `/api/snapshots` | List all snapshots | | `GET` | `/api/snapshots/:name` | Get snapshot by name | ### System @@ -125,7 +170,7 @@ Notes: | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/health` | Health check | -| `GET` | `/api/sync/status` | Memory/WebSocket stats | +| `GET` | `/api/sync/status` | Element count and WebSocket stats | ## Skill Scripts @@ -140,3 +185,11 @@ node scripts/create-element.cjs --data '{...}' node scripts/update-element.cjs --id --data '{...}' node scripts/delete-element.cjs --id ``` + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `CANVAS_PORT` | `3000` | Port the canvas server listens on | +| `EXPRESS_SERVER_URL` | `http://localhost:3000` | Full canvas URL (derived from CANVAS_PORT if not set) | +| `EXCALIDRAW_EXPORT_DIR` | `process.cwd()` | Allowed base directory for file exports (path traversal protection) | diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 0000000..c4f9c9b --- /dev/null +++ b/src/db.ts @@ -0,0 +1,509 @@ +import Database from 'better-sqlite3'; +import path from 'path'; +import fs from 'fs'; +import os from 'os'; +import logger from './utils/logger.js'; +import type { ServerElement, Snapshot } from './types.js'; + +export interface Tenant { + id: string; + name: string; + workspace_path: string; + created_at: string; + last_accessed_at: string; +} + +export interface Project { + id: string; + name: string; + description: string | null; + tenant_id: string; + created_at: string; + updated_at: string; +} + +export interface ElementVersion { + id: number; + element_id: string; + project_id: string; + version: number; + data: ServerElement; + operation: 'create' | 'update' | 'delete'; + created_at: string; +} + +const DEFAULT_PROJECT_ID = 'default'; +const DEFAULT_TENANT_ID = 'default'; + +let db: Database.Database; +let activeTenantId: string = DEFAULT_TENANT_ID; +let activeProjectId: string = DEFAULT_PROJECT_ID; + +function generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).substring(2); +} + +export function initDb(dbPath?: string): void { + const resolvedPath = dbPath + || process.env.EXCALIDRAW_DB_PATH + || path.join(os.homedir(), '.excalidraw-mcp', 'excalidraw.db'); + + const dir = path.dirname(resolvedPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + db = new Database(resolvedPath); + db.pragma('journal_mode = WAL'); + db.pragma('busy_timeout = 5000'); + db.pragma('foreign_keys = ON'); + + runMigrations(); + + // Ensure default tenant exists + const defaultTenant = db.prepare('SELECT id FROM tenants WHERE id = ?').get(DEFAULT_TENANT_ID); + if (!defaultTenant) { + const now = new Date().toISOString(); + db.prepare('INSERT INTO tenants (id, name, workspace_path, created_at, last_accessed_at) VALUES (?, ?, ?, ?, ?)').run( + DEFAULT_TENANT_ID, 'Default', '(none)', now, now + ); + } + + // Ensure default project exists and is linked to default tenant + const defaultProject = db.prepare('SELECT id FROM projects WHERE id = ?').get(DEFAULT_PROJECT_ID); + if (!defaultProject) { + db.prepare('INSERT INTO projects (id, name, description, tenant_id) VALUES (?, ?, ?, ?)').run( + DEFAULT_PROJECT_ID, 'Default', 'Default project', DEFAULT_TENANT_ID + ); + } + + logger.info(`SQLite database initialized at ${resolvedPath}`); +} + +function runMigrations(): void { + db.exec(` + CREATE TABLE IF NOT EXISTS tenants ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + workspace_path TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + last_accessed_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS elements ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + type TEXT NOT NULL, + data TEXT NOT NULL, + label_text TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + is_deleted INTEGER NOT NULL DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS element_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + element_id TEXT NOT NULL, + project_id TEXT NOT NULL, + version INTEGER NOT NULL, + data TEXT NOT NULL, + operation TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + name TEXT NOT NULL, + elements TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(project_id, name) + ); + + CREATE INDEX IF NOT EXISTS idx_elements_project ON elements(project_id); + CREATE INDEX IF NOT EXISTS idx_elements_type ON elements(project_id, type); + CREATE INDEX IF NOT EXISTS idx_elements_deleted ON elements(project_id, is_deleted); + CREATE INDEX IF NOT EXISTS idx_versions_element ON element_versions(element_id); + CREATE INDEX IF NOT EXISTS idx_versions_project ON element_versions(project_id, created_at); + `); + + // FTS table + const ftsExists = db.prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name='elements_fts'" + ).get(); + + if (!ftsExists) { + db.exec(` + CREATE VIRTUAL TABLE elements_fts USING fts5( + element_id, + label_text, + type + ); + `); + } + + // Migration: add tenant_id to projects if it doesn't exist (upgrading from older schema) + const cols = db.prepare("PRAGMA table_info(projects)").all() as { name: string }[]; + const hasTenantCol = cols.some(c => c.name === 'tenant_id'); + if (!hasTenantCol) { + db.exec(`ALTER TABLE projects ADD COLUMN tenant_id TEXT REFERENCES tenants(id)`); + logger.info('Migrated: added tenant_id column to projects'); + } + + db.exec(`CREATE INDEX IF NOT EXISTS idx_projects_tenant ON projects(tenant_id)`); + + // Migration: assign orphan projects (no tenant_id) to default tenant + const orphans = db.prepare('SELECT id FROM projects WHERE tenant_id IS NULL').all() as { id: string }[]; + if (orphans.length > 0) { + // Ensure default tenant exists for migration + const defTenant = db.prepare('SELECT id FROM tenants WHERE id = ?').get(DEFAULT_TENANT_ID); + if (!defTenant) { + const now = new Date().toISOString(); + db.prepare('INSERT INTO tenants (id, name, workspace_path, created_at, last_accessed_at) VALUES (?, ?, ?, ?, ?)').run( + DEFAULT_TENANT_ID, 'Default', '(none)', now, now + ); + } + db.prepare('UPDATE projects SET tenant_id = ? WHERE tenant_id IS NULL').run(DEFAULT_TENANT_ID); + logger.info(`Migrated: assigned ${orphans.length} orphan projects to default tenant`); + } +} + +function extractLabelText(element: ServerElement): string | null { + if (element.label?.text) return element.label.text; + if (element.text) return element.text; + return null; +} + +// Resolve effective project ID: explicit override > in-memory active +function pid(override?: string): string { + return override ?? activeProjectId; +} + +// Given a tenant ID, return its default project (creating one if needed) +export function getDefaultProjectForTenant(tenantId: string): string { + const row = db.prepare( + 'SELECT id FROM projects WHERE tenant_id = ? ORDER BY created_at ASC LIMIT 1' + ).get(tenantId) as { id: string } | undefined; + + if (row) return row.id; + + const id = `${tenantId}-default`; + const now = new Date().toISOString(); + db.prepare( + 'INSERT INTO projects (id, name, description, tenant_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)' + ).run(id, 'Default', 'Default project', tenantId, now, now); + return id; +} + +// ── Element CRUD ── + +export function getElement(id: string, projectId?: string): ServerElement | undefined { + const row = db.prepare( + 'SELECT data FROM elements WHERE id = ? AND project_id = ? AND is_deleted = 0' + ).get(id, pid(projectId)) as { data: string } | undefined; + return row ? JSON.parse(row.data) : undefined; +} + +export function hasElement(id: string, projectId?: string): boolean { + const row = db.prepare( + 'SELECT 1 FROM elements WHERE id = ? AND project_id = ? AND is_deleted = 0' + ).get(id, pid(projectId)); + return !!row; +} + +export function setElement(id: string, element: ServerElement, projectId?: string): void { + const p = pid(projectId); + const now = new Date().toISOString(); + const data = JSON.stringify(element); + const labelText = extractLabelText(element); + const existing = db.prepare( + 'SELECT version, is_deleted FROM elements WHERE id = ? AND project_id = ?' + ).get(id, p) as { version: number; is_deleted: number } | undefined; + + if (existing) { + const newVersion = existing.is_deleted ? 1 : (existing.version + 1); + db.prepare(` + UPDATE elements SET type = ?, data = ?, label_text = ?, updated_at = ?, version = ?, is_deleted = 0 + WHERE id = ? AND project_id = ? + `).run(element.type, data, labelText, now, newVersion, id, p); + + recordVersion(id, newVersion, data, existing.is_deleted ? 'create' : 'update', p); + updateFts(id, labelText, element.type); + } else { + db.prepare(` + INSERT INTO elements (id, project_id, type, data, label_text, created_at, updated_at, version) + VALUES (?, ?, ?, ?, ?, ?, ?, 1) + `).run(id, p, element.type, data, labelText, now, now); + + recordVersion(id, 1, data, 'create', p); + insertFts(id, labelText, element.type); + } +} + +export function deleteElement(id: string, projectId?: string): boolean { + const p = pid(projectId); + const existing = db.prepare( + 'SELECT version, data FROM elements WHERE id = ? AND project_id = ? AND is_deleted = 0' + ).get(id, p) as { version: number; data: string } | undefined; + + if (!existing) return false; + + const newVersion = existing.version + 1; + db.prepare(` + UPDATE elements SET is_deleted = 1, version = ?, updated_at = ? + WHERE id = ? AND project_id = ? + `).run(newVersion, new Date().toISOString(), id, p); + + recordVersion(id, newVersion, existing.data, 'delete', p); + deleteFts(id); + return true; +} + +export function getAllElements(projectId?: string): ServerElement[] { + const rows = db.prepare( + 'SELECT data FROM elements WHERE project_id = ? AND is_deleted = 0' + ).all(pid(projectId)) as { data: string }[]; + return rows.map(r => JSON.parse(r.data)); +} + +export function getElementCount(projectId?: string): number { + const row = db.prepare( + 'SELECT COUNT(*) as count FROM elements WHERE project_id = ? AND is_deleted = 0' + ).get(pid(projectId)) as { count: number }; + return row.count; +} + +export function clearElements(projectId?: string): number { + const p = pid(projectId); + const now = new Date().toISOString(); + const elements = getAllElements(p); + + const stmt = db.prepare(` + UPDATE elements SET is_deleted = 1, version = version + 1, updated_at = ? + WHERE project_id = ? AND is_deleted = 0 + `); + + const clearTx = db.transaction(() => { + const info = stmt.run(now, p); + for (const el of elements) { + recordVersion(el.id, (el.version || 1) + 1, JSON.stringify(el), 'delete', p); + deleteFts(el.id); + } + return info.changes; + }); + + return clearTx() as number; +} + +export function queryElements(type?: string, filter?: Record, projectId?: string): ServerElement[] { + let elements = getAllElements(projectId); + if (type) { + elements = elements.filter(el => el.type === type); + } + if (filter) { + elements = elements.filter(el => { + return Object.entries(filter).every(([key, value]) => { + return (el as any)[key] === value; + }); + }); + } + return elements; +} + +export function searchElements(query: string, projectId?: string): ServerElement[] { + const rows = db.prepare(` + SELECT e.data FROM elements e + INNER JOIN elements_fts fts ON fts.element_id = e.id + WHERE elements_fts MATCH ? AND e.project_id = ? AND e.is_deleted = 0 + `).all(query, pid(projectId)) as { data: string }[]; + return rows.map(r => JSON.parse(r.data)); +} + +// ── FTS helpers ── + +function insertFts(elementId: string, labelText: string | null, type: string): void { + db.prepare('INSERT INTO elements_fts (element_id, label_text, type) VALUES (?, ?, ?)').run( + elementId, labelText || '', type + ); +} + +function updateFts(elementId: string, labelText: string | null, type: string): void { + deleteFts(elementId); + insertFts(elementId, labelText, type); +} + +function deleteFts(elementId: string): void { + db.prepare("DELETE FROM elements_fts WHERE element_id = ?").run(elementId); +} + +// ── Version history ── + +function recordVersion(elementId: string, version: number, data: string, operation: string, projectId?: string): void { + db.prepare(` + INSERT INTO element_versions (element_id, project_id, version, data, operation) + VALUES (?, ?, ?, ?, ?) + `).run(elementId, pid(projectId), version, data, operation); +} + +export function getElementHistory(elementId: string, limit: number = 50, projectId?: string): ElementVersion[] { + const rows = db.prepare(` + SELECT id, element_id, project_id, version, data, operation, created_at + FROM element_versions WHERE element_id = ? AND project_id = ? + ORDER BY created_at DESC LIMIT ? + `).all(elementId, pid(projectId), limit) as any[]; + return rows.map(r => ({ ...r, data: JSON.parse(r.data) })); +} + +export function getProjectHistory(limit: number = 100, projectId?: string): ElementVersion[] { + const rows = db.prepare(` + SELECT id, element_id, project_id, version, data, operation, created_at + FROM element_versions WHERE project_id = ? + ORDER BY created_at DESC LIMIT ? + `).all(pid(projectId), limit) as any[]; + return rows.map(r => ({ ...r, data: JSON.parse(r.data) })); +} + +// ── Snapshots ── + +export function saveSnapshot(name: string, elements: ServerElement[], projectId?: string): void { + const data = JSON.stringify(elements); + db.prepare(` + INSERT OR REPLACE INTO snapshots (project_id, name, elements, created_at) + VALUES (?, ?, ?, datetime('now')) + `).run(pid(projectId), name, data); +} + +export function getSnapshot(name: string, projectId?: string): Snapshot | undefined { + const row = db.prepare( + 'SELECT name, elements, created_at FROM snapshots WHERE name = ? AND project_id = ?' + ).get(name, pid(projectId)) as { name: string; elements: string; created_at: string } | undefined; + + if (!row) return undefined; + return { name: row.name, elements: JSON.parse(row.elements), createdAt: row.created_at }; +} + +export function listSnapshots(projectId?: string): { name: string; elementCount: number; createdAt: string }[] { + const rows = db.prepare( + 'SELECT name, elements, created_at FROM snapshots WHERE project_id = ? ORDER BY created_at DESC' + ).all(pid(projectId)) as { name: string; elements: string; created_at: string }[]; + return rows.map(r => ({ + name: r.name, + elementCount: (JSON.parse(r.elements) as any[]).length, + createdAt: r.created_at + })); +} + +// ── Tenants ── + +export function ensureTenant(id: string, name: string, workspacePath: string): Tenant { + const now = new Date().toISOString(); + const existing = db.prepare('SELECT * FROM tenants WHERE id = ?').get(id) as Tenant | undefined; + + if (existing) { + db.prepare('UPDATE tenants SET last_accessed_at = ? WHERE id = ?').run(now, id); + return { ...existing, last_accessed_at: now }; + } + + db.prepare( + 'INSERT INTO tenants (id, name, workspace_path, created_at, last_accessed_at) VALUES (?, ?, ?, ?, ?)' + ).run(id, name, workspacePath, now, now); + + return { id, name, workspace_path: workspacePath, created_at: now, last_accessed_at: now }; +} + +export function setActiveTenant(id: string): void { + const tenant = db.prepare('SELECT id FROM tenants WHERE id = ?').get(id); + if (!tenant) throw new Error(`Tenant "${id}" not found`); + activeTenantId = id; + + // Auto-set active project to the tenant's first project, creating a default if none exists + const firstProject = db.prepare( + 'SELECT id FROM projects WHERE tenant_id = ? ORDER BY created_at ASC LIMIT 1' + ).get(id) as { id: string } | undefined; + + if (firstProject) { + activeProjectId = firstProject.id; + } else { + const defaultId = `${id}-default`; + const now = new Date().toISOString(); + db.prepare( + 'INSERT INTO projects (id, name, description, tenant_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)' + ).run(defaultId, 'Default', 'Default project', id, now, now); + activeProjectId = defaultId; + } + + logger.info(`Active tenant set to "${id}", active project: "${activeProjectId}"`); +} + +export function getActiveTenant(): Tenant { + return db.prepare('SELECT * FROM tenants WHERE id = ?').get(activeTenantId) as Tenant; +} + +export function getActiveTenantId(): string { + return activeTenantId; +} + +export function listTenants(): Tenant[] { + return db.prepare('SELECT * FROM tenants ORDER BY last_accessed_at DESC').all() as Tenant[]; +} + +// ── Projects ── + +export function createProject(name: string, description?: string): Project { + const id = generateId(); + const now = new Date().toISOString(); + db.prepare( + 'INSERT INTO projects (id, name, description, tenant_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)' + ).run(id, name, description || null, activeTenantId, now, now); + return { id, name, description: description || null, tenant_id: activeTenantId, created_at: now, updated_at: now }; +} + +export function listProjects(): Project[] { + return db.prepare('SELECT * FROM projects WHERE tenant_id = ? ORDER BY updated_at DESC').all(activeTenantId) as Project[]; +} + +export function setActiveProject(id: string): void { + const project = db.prepare('SELECT id, tenant_id FROM projects WHERE id = ?').get(id) as { id: string; tenant_id: string } | undefined; + if (!project) throw new Error(`Project "${id}" not found`); + if (project.tenant_id !== activeTenantId) { + throw new Error(`Project "${id}" belongs to tenant "${project.tenant_id}", not the active tenant "${activeTenantId}"`); + } + activeProjectId = id; +} + +export function getActiveProject(): Project { + return db.prepare('SELECT * FROM projects WHERE id = ?').get(activeProjectId) as Project; +} + +export function getActiveProjectId(): string { + return activeProjectId; +} + +// ── Bulk operations (for sync endpoint) ── + +export function bulkReplaceElements(elements: ServerElement[], projectId?: string): number { + const tx = db.transaction(() => { + clearElements(projectId); + for (const el of elements) { + setElement(el.id, el, projectId); + } + return elements.length; + }); + return tx(); +} + +export function closeDb(): void { + if (db) { + db.close(); + logger.info('SQLite database closed'); + } +} diff --git a/src/index.ts b/src/index.ts index 38569d2..da6fb23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ process.env.NO_COLOR = '1'; import { fileURLToPath } from "url"; import { deflateSync } from 'zlib'; -import { webcrypto } from 'crypto'; +import { webcrypto, createHash } from 'crypto'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { @@ -28,6 +28,17 @@ import { validateElement } from './types.js'; import fetch from 'node-fetch'; +import { startCanvasServer, stopCanvasServer } from './server.js'; +import { + initDb, closeDb, + searchElements as dbSearchElements, + listProjects as dbListProjects, createProject as dbCreateProject, + setActiveProject as dbSetActiveProject, getActiveProject as dbGetActiveProject, + getElementHistory as dbGetElementHistory, getProjectHistory as dbGetProjectHistory, + ensureTenant as dbEnsureTenant, setActiveTenant as dbSetActiveTenant, + getActiveTenant as dbGetActiveTenant, getActiveTenantId as dbGetActiveTenantId, + listTenants as dbListTenants +} from './db.js'; // Load environment variables dotenv.config(); @@ -47,9 +58,10 @@ function sanitizeFilePath(filePath: string): string { return resolved; } -// Express server configuration -const EXPRESS_SERVER_URL = process.env.EXPRESS_SERVER_URL || 'http://localhost:3000'; -const ENABLE_CANVAS_SYNC = process.env.ENABLE_CANVAS_SYNC !== 'false'; // Default to true +// Express server configuration — derive URL from CANVAS_PORT +const CANVAS_PORT = process.env.CANVAS_PORT || process.env.PORT || '3000'; +const EXPRESS_SERVER_URL = process.env.EXPRESS_SERVER_URL || `http://localhost:${CANVAS_PORT}`; +const ENABLE_CANVAS_SYNC = true; // API Response types interface ApiResponse { @@ -66,6 +78,14 @@ interface SyncResponse { elements?: ServerElement[]; } +function canvasHeaders(extra?: Record): Record { + return { + 'Content-Type': 'application/json', + 'X-Tenant-Id': dbGetActiveTenantId(), + ...extra + }; +} + // Helper functions to sync with Express server (canvas) async function syncToCanvas(operation: string, data: any): Promise { if (!ENABLE_CANVAS_SYNC) { @@ -82,7 +102,7 @@ async function syncToCanvas(operation: string, data: any): Promise 0 + ? `Found ${results.length} matching elements:\n\n${JSON.stringify(results, null, 2)}` + : `No elements found matching "${params.query}"` + }] + }; + } + + case 'list_projects': { + logger.info('Listing projects via MCP'); + const projects = dbListProjects(); + const active = dbGetActiveProject(); + return { + content: [{ + type: 'text', + text: `Active project: ${active.name} (${active.id})\n\nAll projects:\n${JSON.stringify(projects, null, 2)}` + }] + }; + } + + case 'switch_project': { + const params = z.object({ + projectId: z.string().optional(), + createName: z.string().optional(), + createDescription: z.string().optional() + }).parse(args || {}); + + if (params.createName) { + const newProject = dbCreateProject(params.createName, params.createDescription); + dbSetActiveProject(newProject.id); + logger.info('Created and switched to new project', { project: newProject }); + return { + content: [{ + type: 'text', + text: `Created new project "${newProject.name}" and switched to it.\n\n${JSON.stringify(newProject, null, 2)}` + }] + }; + } + + if (params.projectId) { + dbSetActiveProject(params.projectId); + const active = dbGetActiveProject(); + logger.info('Switched project', { project: active }); + return { + content: [{ + type: 'text', + text: `Switched to project "${active.name}" (${active.id})` + }] + }; + } + + throw new Error('Provide either projectId to switch to or createName to create a new project'); + } + + case 'element_history': { + const params = z.object({ + elementId: z.string().optional(), + limit: z.number().optional() + }).parse(args || {}); + + const limit = params.limit ?? 50; + + if (params.elementId) { + const history = dbGetElementHistory(params.elementId, limit); + return { + content: [{ + type: 'text', + text: history.length > 0 + ? `Version history for element ${params.elementId} (${history.length} entries):\n\n${JSON.stringify(history, null, 2)}` + : `No history found for element ${params.elementId}` + }] + }; + } + + const history = dbGetProjectHistory(limit); + const active = dbGetActiveProject(); + return { + content: [{ + type: 'text', + text: history.length > 0 + ? `Project history for "${active.name}" (${history.length} entries):\n\n${JSON.stringify(history, null, 2)}` + : `No history in project "${active.name}"` + }] + }; + } + + case 'list_tenants': { + logger.info('Listing tenants via MCP'); + const tenants = dbListTenants(); + const activeTenant = dbGetActiveTenant(); + return { + content: [{ + type: 'text', + text: `Active tenant: ${activeTenant.name} (${activeTenant.id})\nWorkspace: ${activeTenant.workspace_path}\n\nAll tenants:\n${JSON.stringify(tenants, null, 2)}` + }] + }; + } + + case 'switch_tenant': { + const params = z.object({ tenantId: z.string() }).parse(args); + logger.info('Switching tenant via MCP', { tenantId: params.tenantId }); + + dbSetActiveTenant(params.tenantId); + const tenant = dbGetActiveTenant(); + const activeProject = dbGetActiveProject(); + + try { + await fetch(`${EXPRESS_SERVER_URL}/api/tenant/active`, { + method: 'PUT', + headers: canvasHeaders(), + body: JSON.stringify({ tenantId: params.tenantId }) + }); + } catch {} + + return { + content: [{ + type: 'text', + text: `Switched to tenant "${tenant.name}" (${tenant.id})\nWorkspace: ${tenant.workspace_path}\nActive project: ${activeProject.name} (${activeProject.id})` + }] + }; + } + default: throw new Error(`Unknown tool: ${name}`); } @@ -2193,12 +2431,74 @@ async function runServer(): Promise { try { logger.info('Starting Excalidraw MCP server...'); + // Initialize SQLite before anything else + initDb(); + + // Bootstrap tenant from process.cwd() (may be home dir for global MCPs) + let workspacePath = process.cwd(); + + function applyTenant(wp: string) { + const tid = createHash('sha256').update(wp).digest('hex').slice(0, 12); + const tname = path.basename(wp); + dbEnsureTenant(tid, tname, wp); + dbSetActiveTenant(tid); + logger.info(`Tenant initialized: "${tname}" (${tid}) from ${wp}`); + return { tenantId: tid, tenantName: tname }; + } + + applyTenant(workspacePath); + + try { + await startCanvasServer(); + logger.info('Canvas server started — lifecycle managed by MCP process'); + } catch (canvasError) { + logger.warn('Canvas server failed to start:', (canvasError as Error).message); + logger.warn('MCP tools will work without real-time canvas sync'); + } + const transport = new StdioServerTransport(); logger.debug('Connecting to stdio transport...'); await server.connect(transport); logger.info('Excalidraw MCP server running on stdio'); + // After connecting, ask the client for the real workspace roots. + // Global MCPs often get cwd=HOME; roots gives us the actual workspace. + try { + const { roots } = await server.listRoots(undefined, { timeout: 5_000 }); + if (roots && roots.length > 0) { + const rootUri = roots[0]!.uri; + const rootPath = rootUri.startsWith('file://') ? decodeURIComponent(rootUri.slice(7)) : rootUri; + if (rootPath && rootPath !== workspacePath) { + logger.info(`Client reported workspace root: ${rootPath} (was ${workspacePath})`); + workspacePath = rootPath; + const { tenantId: newTid } = applyTenant(workspacePath); + + try { + await fetch(`${EXPRESS_SERVER_URL}/api/tenant/active`, { + method: 'PUT', + headers: canvasHeaders(), + body: JSON.stringify({ tenantId: newTid }) + }); + } catch {} + } + } + } catch (rootsErr) { + logger.debug('Could not retrieve roots from client (not supported or timed out):', (rootsErr as Error).message); + } + + async function shutdown() { + logger.info('MCP transport closed — shutting down'); + try { await stopCanvasServer(); } catch {} + try { closeDb(); } catch {} + process.exit(0); + } + + server.onclose = shutdown; + process.stdin.on('close', shutdown); + process.on('SIGTERM', shutdown); + process.on('SIGINT', shutdown); + process.stdin.resume(); } catch (error) { logger.error('Error starting server:', error); diff --git a/src/server.ts b/src/server.ts index 4ae1a33..3cd3098 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,4 @@ -import express, { Request, Response, NextFunction } from 'express'; +import express, { type Application, Request, Response, NextFunction } from 'express'; import cors from 'cors'; import { WebSocketServer } from 'ws'; import { createServer } from 'http'; @@ -7,8 +7,6 @@ import { fileURLToPath } from 'url'; import dotenv from 'dotenv'; import logger from './utils/logger.js'; import { - elements, - snapshots, generateId, EXCALIDRAW_ELEMENT_TYPES, ServerElement, @@ -22,6 +20,8 @@ import { InitialElementsMessage, Snapshot } from './types.js'; +import * as store from './db.js'; +import { listTenants as dbListTenants, getActiveTenant as dbGetActiveTenant, setActiveTenant as dbSetActiveTenant, getDefaultProjectForTenant } from './db.js'; import { z } from 'zod'; import WebSocket from 'ws'; @@ -31,9 +31,9 @@ dotenv.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const app = express(); -const server = createServer(app); -const wss = new WebSocketServer({ server }); +const app: Application = express(); +const httpServer = createServer(app); +const wss = new WebSocketServer({ server: httpServer }); // Middleware app.use(cors()); @@ -45,6 +45,14 @@ app.use(express.static(staticDir)); // Also serve frontend assets app.use(express.static(path.join(__dirname, '../dist/frontend'))); +// Resolve tenant from X-Tenant-Id header to a projectId override. +// Returns undefined when header is absent (browser requests), falling back to global state. +function resolveTenantProject(req: Request): string | undefined { + const tenantId = req.headers['x-tenant-id'] as string | undefined; + if (!tenantId) return undefined; + return getDefaultProjectForTenant(tenantId); +} + // WebSocket connections const clients = new Set(); @@ -62,18 +70,27 @@ function broadcast(message: WebSocketMessage): void { wss.on('connection', (ws: WebSocket) => { clients.add(ws); logger.info('New WebSocket connection established'); + + // Send current tenant info + try { + const tenant = dbGetActiveTenant(); + ws.send(JSON.stringify({ + type: 'tenant_switched', + tenant: { id: tenant.id, name: tenant.name, workspace_path: tenant.workspace_path } + })); + } catch {} // Send current elements to new client const initialMessage: InitialElementsMessage = { type: 'initial_elements', - elements: Array.from(elements.values()) + elements: store.getAllElements() }; ws.send(JSON.stringify(initialMessage)); // Send sync status to new client const syncMessage: SyncStatusMessage = { type: 'sync_status', - elementCount: elements.size, + elementCount: store.getElementCount(), timestamp: new Date().toISOString() }; ws.send(JSON.stringify(syncMessage)); @@ -161,11 +178,12 @@ const UpdateElementSchema = z.object({ // Get all elements app.get('/api/elements', (req: Request, res: Response) => { try { - const elementsArray = Array.from(elements.values()); + const projId = resolveTenantProject(req); + const allElements = store.getAllElements(projId); res.json({ success: true, - elements: elementsArray, - count: elementsArray.length + elements: allElements, + count: allElements.length }); } catch (error) { logger.error('Error fetching elements:', error); @@ -179,10 +197,10 @@ app.get('/api/elements', (req: Request, res: Response) => { // Create new element app.post('/api/elements', (req: Request, res: Response) => { try { + const projId = resolveTenantProject(req); const params = CreateElementSchema.parse(req.body); logger.info('Creating element via API', { type: params.type }); - // Prioritize passed ID (for MCP sync), otherwise generate new ID const id = params.id || generateId(); const element: ServerElement = { id, @@ -192,9 +210,8 @@ app.post('/api/elements', (req: Request, res: Response) => { version: 1 }; - elements.set(id, element); + store.setElement(id, element, projId); - // Broadcast to all connected clients const message: ElementCreatedMessage = { type: 'element_created', element: element @@ -217,6 +234,7 @@ app.post('/api/elements', (req: Request, res: Response) => { // Update element app.put('/api/elements/:id', (req: Request, res: Response) => { try { + const projId = resolveTenantProject(req); const { id } = req.params; const updates = UpdateElementSchema.parse({ id, ...req.body }); @@ -227,7 +245,7 @@ app.put('/api/elements/:id', (req: Request, res: Response) => { }); } - const existingElement = elements.get(id); + const existingElement = store.getElement(id, projId); if (!existingElement) { return res.status(404).json({ success: false, @@ -242,9 +260,8 @@ app.put('/api/elements/:id', (req: Request, res: Response) => { version: (existingElement.version || 0) + 1 }; - elements.set(id, updatedElement); + store.setElement(id, updatedElement, projId); - // Broadcast to all connected clients const message: ElementUpdatedMessage = { type: 'element_updated', element: updatedElement @@ -267,8 +284,8 @@ app.put('/api/elements/:id', (req: Request, res: Response) => { // Clear all elements (must be before /:id route) app.delete('/api/elements/clear', (req: Request, res: Response) => { try { - const count = elements.size; - elements.clear(); + const projId = resolveTenantProject(req); + const count = store.clearElements(projId); broadcast({ type: 'canvas_cleared', @@ -294,6 +311,7 @@ app.delete('/api/elements/clear', (req: Request, res: Response) => { // Delete element app.delete('/api/elements/:id', (req: Request, res: Response) => { try { + const projId = resolveTenantProject(req); const { id } = req.params; if (!id) { @@ -303,14 +321,14 @@ app.delete('/api/elements/:id', (req: Request, res: Response) => { }); } - if (!elements.has(id)) { + if (!store.hasElement(id, projId)) { return res.status(404).json({ success: false, error: `Element with ID ${id} not found` }); } - elements.delete(id); + store.deleteElement(id, projId); // Broadcast to all connected clients const message: ElementDeletedMessage = { @@ -335,22 +353,19 @@ app.delete('/api/elements/:id', (req: Request, res: Response) => { // Query elements with filters app.get('/api/elements/search', (req: Request, res: Response) => { try { - const { type, ...filters } = req.query; - let results = Array.from(elements.values()); - - // Filter by type if specified - if (type && typeof type === 'string') { - results = results.filter(element => element.type === type); - } - - // Apply additional filters - if (Object.keys(filters).length > 0) { - results = results.filter(element => { - return Object.entries(filters).every(([key, value]) => { - return (element as any)[key] === value; - }); - }); + const projId = resolveTenantProject(req); + const { type, q, ...filters } = req.query; + + if (q && typeof q === 'string') { + const results = store.searchElements(q, projId); + return res.json({ success: true, elements: results, count: results.length }); } + + const results = store.queryElements( + type && typeof type === 'string' ? type : undefined, + Object.keys(filters).length > 0 ? filters as Record : undefined, + projId + ); res.json({ success: true, @@ -369,6 +384,7 @@ app.get('/api/elements/search', (req: Request, res: Response) => { // Get element by ID app.get('/api/elements/:id', (req: Request, res: Response) => { try { + const projId = resolveTenantProject(req); const { id } = req.params; if (!id) { @@ -378,7 +394,7 @@ app.get('/api/elements/:id', (req: Request, res: Response) => { }); } - const element = elements.get(id); + const element = store.getElement(id, projId); if (!element) { return res.status(404).json({ @@ -453,14 +469,14 @@ function computeEdgePoint( } // Helper: resolve arrow bindings in a batch -function resolveArrowBindings(batchElements: ServerElement[]): void { +function resolveArrowBindings(batchElements: ServerElement[], projectId?: string): void { const elementMap = new Map(); batchElements.forEach(el => elementMap.set(el.id, el)); // Also check existing elements for cross-batch references - elements.forEach((el, id) => { - if (!elementMap.has(id)) elementMap.set(id, el); - }); + for (const el of store.getAllElements(projectId)) { + if (!elementMap.has(el.id)) elementMap.set(el.id, el); + } for (const el of batchElements) { if (el.type !== 'arrow' && el.type !== 'line') continue; @@ -535,6 +551,7 @@ function resolveArrowBindings(batchElements: ServerElement[]): void { // Batch create elements app.post('/api/elements/batch', (req: Request, res: Response) => { try { + const projId = resolveTenantProject(req); const { elements: elementsToCreate } = req.body; if (!Array.isArray(elementsToCreate)) { @@ -548,7 +565,6 @@ app.post('/api/elements/batch', (req: Request, res: Response) => { elementsToCreate.forEach(elementData => { const params = CreateElementSchema.parse(elementData); - // Prioritize passed ID (for MCP sync), otherwise generate new ID const id = params.id || generateId(); const element: ServerElement = { id, @@ -561,11 +577,9 @@ app.post('/api/elements/batch', (req: Request, res: Response) => { createdElements.push(element); }); - // Resolve arrow bindings (computes positions, startBinding, endBinding, boundElements) - resolveArrowBindings(createdElements); + resolveArrowBindings(createdElements, projId); - // Store all elements after binding resolution - createdElements.forEach(el => elements.set(el.id, el)); + createdElements.forEach(el => store.setElement(el.id, el, projId)); // Broadcast to all connected clients const message: BatchCreatedMessage = { @@ -632,6 +646,7 @@ app.post('/api/elements/from-mermaid', (req: Request, res: Response) => { // Sync elements from frontend (overwrite sync) app.post('/api/elements/sync', (req: Request, res: Response) => { try { + const projId = resolveTenantProject(req); const { elements: frontendElements, timestamp } = req.body; logger.info(`Sync request received: ${frontendElements.length} elements`, { @@ -639,7 +654,6 @@ app.post('/api/elements/sync', (req: Request, res: Response) => { elementCount: frontendElements.length }); - // Validate input data if (!Array.isArray(frontendElements)) { return res.status(400).json({ success: false, @@ -647,23 +661,15 @@ app.post('/api/elements/sync', (req: Request, res: Response) => { }); } - // Record element count before sync - const beforeCount = elements.size; - - // 1. Clear existing memory storage - elements.clear(); - logger.info(`Cleared existing elements: ${beforeCount} elements removed`); - - // 2. Batch write new data - let successCount = 0; + const beforeCount = store.getElementCount(projId); + + // Process elements with server metadata const processedElements: ServerElement[] = []; - + let successCount = 0; + frontendElements.forEach((element: any, index: number) => { try { - // Ensure element has ID, generate one if missing const elementId = element.id || generateId(); - - // Add server metadata const processedElement: ServerElement = { ...element, id: elementId, @@ -672,35 +678,30 @@ app.post('/api/elements/sync', (req: Request, res: Response) => { syncTimestamp: timestamp, version: 1 }; - - // Store to memory - elements.set(elementId, processedElement); processedElements.push(processedElement); successCount++; - } catch (elementError) { logger.warn(`Failed to process element ${index}:`, elementError); } }); - + + store.bulkReplaceElements(processedElements, projId); logger.info(`Sync completed: ${successCount}/${frontendElements.length} elements synced`); - - // 3. Broadcast sync event to all WebSocket clients + broadcast({ type: 'elements_synced', count: successCount, timestamp: new Date().toISOString(), source: 'manual_sync' }); - - // 4. Return sync results + res.json({ success: true, message: `Successfully synced ${successCount} elements`, count: successCount, syncedAt: new Date().toISOString(), beforeCount, - afterCount: elements.size + afterCount: store.getElementCount(projId) }); } catch (error) { @@ -919,6 +920,7 @@ app.post('/api/viewport/result', (req: Request, res: Response) => { // Snapshots: save app.post('/api/snapshots', (req: Request, res: Response) => { try { + const projId = resolveTenantProject(req); const { name } = req.body; if (!name || typeof name !== 'string') { @@ -928,20 +930,15 @@ app.post('/api/snapshots', (req: Request, res: Response) => { }); } - const snapshot: Snapshot = { - name, - elements: Array.from(elements.values()), - createdAt: new Date().toISOString() - }; - - snapshots.set(name, snapshot); - logger.info(`Snapshot saved: "${name}" with ${snapshot.elements.length} elements`); + const allElements = store.getAllElements(projId); + store.saveSnapshot(name, allElements, projId); + logger.info(`Snapshot saved: "${name}" with ${allElements.length} elements`); res.json({ success: true, name, - elementCount: snapshot.elements.length, - createdAt: snapshot.createdAt + elementCount: allElements.length, + createdAt: new Date().toISOString() }); } catch (error) { logger.error('Error saving snapshot:', error); @@ -955,11 +952,8 @@ app.post('/api/snapshots', (req: Request, res: Response) => { // Snapshots: list app.get('/api/snapshots', (req: Request, res: Response) => { try { - const list = Array.from(snapshots.values()).map(s => ({ - name: s.name, - elementCount: s.elements.length, - createdAt: s.createdAt - })); + const projId = resolveTenantProject(req); + const list = store.listSnapshots(projId); res.json({ success: true, @@ -978,8 +972,9 @@ app.get('/api/snapshots', (req: Request, res: Response) => { // Snapshots: get by name app.get('/api/snapshots/:name', (req: Request, res: Response) => { try { + const projId = resolveTenantProject(req); const { name } = req.params; - const snapshot = snapshots.get(name!); + const snapshot = store.getSnapshot(name!, projId); if (!snapshot) { return res.status(404).json({ @@ -1012,21 +1007,68 @@ app.get('/', (req: Request, res: Response) => { }); }); +// ── Tenant API ── + +app.get('/api/tenants', (req: Request, res: Response) => { + try { + const tenants = dbListTenants(); + const active = dbGetActiveTenant(); + res.json({ success: true, tenants, activeTenantId: active.id }); + } catch (error) { + logger.error('Error listing tenants:', error); + res.status(500).json({ success: false, error: (error as Error).message }); + } +}); + +app.get('/api/tenant/active', (req: Request, res: Response) => { + try { + const tenant = dbGetActiveTenant(); + res.json({ success: true, tenant }); + } catch (error) { + logger.error('Error getting active tenant:', error); + res.status(500).json({ success: false, error: (error as Error).message }); + } +}); + +app.put('/api/tenant/active', (req: Request, res: Response) => { + try { + const { tenantId } = req.body; + if (!tenantId || typeof tenantId !== 'string') { + return res.status(400).json({ success: false, error: 'tenantId is required' }); + } + + dbSetActiveTenant(tenantId); + const tenant = dbGetActiveTenant(); + + broadcast({ + type: 'tenant_switched', + tenant: { id: tenant.id, name: tenant.name, workspace_path: tenant.workspace_path } + }); + + res.json({ success: true, tenant }); + } catch (error) { + logger.error('Error switching tenant:', error); + res.status(400).json({ success: false, error: (error as Error).message }); + } +}); + // Health check endpoint app.get('/health', (req: Request, res: Response) => { + const projId = resolveTenantProject(req); res.json({ status: 'healthy', timestamp: new Date().toISOString(), - elements_count: elements.size, + elements_count: store.getElementCount(projId), websocket_clients: clients.size }); }); // Sync status endpoint app.get('/api/sync/status', (req: Request, res: Response) => { + const projId = resolveTenantProject(req); res.json({ success: true, - elementCount: elements.size, + elementCount: store.getElementCount(projId), timestamp: new Date().toISOString(), memoryUsage: { heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), // MB @@ -1045,13 +1087,40 @@ app.use((err: Error, req: Request, res: Response, next: NextFunction) => { }); }); -// Start server -const PORT = parseInt(process.env.PORT || '3000', 10); +// Server configuration +const PORT = parseInt(process.env.CANVAS_PORT || process.env.PORT || '3000', 10); const HOST = process.env.HOST || 'localhost'; -server.listen(PORT, HOST, () => { - logger.info(`POC server running on http://${HOST}:${PORT}`); - logger.info(`WebSocket server running on ws://${HOST}:${PORT}`); -}); +export function startCanvasServer(): Promise { + return new Promise((resolve, reject) => { + const onError = (err: Error) => { + httpServer.removeListener('error', onError); + reject(err); + }; + httpServer.on('error', onError); + + httpServer.listen(PORT, HOST, () => { + httpServer.removeListener('error', onError); + logger.info(`Canvas server running on http://${HOST}:${PORT}`); + logger.info(`WebSocket server running on ws://${HOST}:${PORT}`); + resolve(); + }); + }); +} + +export function stopCanvasServer(): Promise { + return new Promise((resolve) => { + clients.forEach(c => c.close()); + httpServer.close(() => resolve()); + }); +} + +// Direct execution: `node dist/server.js` still works standalone +if (fileURLToPath(import.meta.url) === process.argv[1]) { + startCanvasServer().catch((err) => { + logger.error('Failed to start canvas server:', err); + process.exit(1); + }); +} export default app; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index b735049..2fc7852 100644 --- a/src/types.ts +++ b/src/types.ts @@ -182,7 +182,8 @@ export type WebSocketMessageType = | 'mermaid_convert' | 'canvas_cleared' | 'export_image_request' - | 'set_viewport'; + | 'set_viewport' + | 'tenant_switched'; export interface InitialElementsMessage extends WebSocketMessage { type: 'initial_elements'; @@ -271,6 +272,16 @@ export interface SetViewportMessage extends WebSocketMessage { offsetY?: number; } +// Tenant switched message +export interface TenantSwitchedMessage extends WebSocketMessage { + type: 'tenant_switched'; + tenant: { + id: string; + name: string; + workspace_path: string; + }; +} + // Snapshot types export interface Snapshot { name: string; @@ -278,11 +289,9 @@ export interface Snapshot { createdAt: string; } -// In-memory storage for Excalidraw elements -export const elements = new Map(); - -// In-memory storage for snapshots -export const snapshots = new Map(); +// Storage is now handled by src/db.ts (SQLite). +// The Map exports below are kept only for backward compatibility with +// standalone server.ts usage; they are NOT used when the DB is active. // Validation function for Excalidraw elements export function validateElement(element: Partial): element is ServerElement {