diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 0000000..e44c9d4
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,28 @@
+FROM debian:bookworm-slim
+
+ARG DEBIAN_FRONTEND=noninteractive
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ build-essential \
+ curl \
+ wget \
+ pkg-config \
+ libssl-dev \
+ ca-certificates \
+ git \
+ git-lfs \
+ netcat-traditional \
+ sudo \
+ && git lfs install \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# Create non-root user
+ARG USERNAME=vscode
+ARG USER_UID=1000
+ARG USER_GID=$USER_UID
+RUN groupadd --force --gid $USER_GID $USERNAME \
+ && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /bin/bash || true \
+ && echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/$USERNAME \
+ && chmod 0440 /etc/sudoers.d/$USERNAME
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..6ba3d49
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,47 @@
+{
+ "name": "mind-map",
+ "build": {
+ "dockerfile": "Dockerfile"
+ },
+ "remoteUser": "vscode",
+ "features": {
+ "ghcr.io/devcontainers/features/go:1": {
+ "version": "latest"
+ },
+ "ghcr.io/devcontainers/features/node:1": {
+ "version": "lts"
+ },
+ "ghcr.io/devcontainers/features/sshd:1": {
+ "version": "latest"
+ }
+ },
+ "postCreateCommand": "go version && node --version",
+ "postAttachCommand": "npm install --prefix webui",
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "golang.Go",
+ "hbenl.vscode-test-explorer",
+ "ethan-reesor.vscode-go-test-adapter",
+ "idered.npm",
+ "qwtel.sqlite-viewer",
+ "dbaeumer.vscode-eslint"
+ ],
+ "settings": {
+ "go.buildTags": "sqlite_fts5",
+ "go.testTags": "sqlite_fts5",
+ "go.buildFlags": ["-tags=sqlite_fts5"],
+ "go.testEnvVars": {
+ "CGO_ENABLED": "1"
+ }
+ }
+ }
+ },
+ "forwardPorts": [8080],
+ "portsAttributes": {
+ "8080": {
+ "label": "mind-map Server",
+ "onAutoForward": "notify"
+ }
+ }
+}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..271151e
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,28 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [main]
+ push:
+ branches: [main]
+
+jobs:
+ check:
+ name: Check & Test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - name: Build and test in devcontainer
+ uses: devcontainers/ci@v0.3
+ with:
+ push: never
+ runCmd: |
+ set -e
+
+ # Build webui
+ cd webui && npm install && npm run build && cd ..
+
+ # Go checks
+ CGO_ENABLED=1 go vet -tags sqlite_fts5 ./...
+ CGO_ENABLED=1 go test -tags sqlite_fts5 ./...
+ CGO_ENABLED=1 go build -tags sqlite_fts5 -o /tmp/mind-map ./cmd/mind-map/
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..c0b5d76
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,104 @@
+name: Release
+
+on:
+ release:
+ types: [created]
+
+permissions:
+ contents: write
+
+jobs:
+ build:
+ name: Build release binaries
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Build all targets in devcontainer
+ uses: devcontainers/ci@v0.3
+ with:
+ push: never
+ runCmd: |
+ set -e
+
+ # Build webui first
+ cd webui && npm install && npm run build && cd ..
+
+ # Install cross-compilation toolchain for linux-arm64
+ sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu
+
+ # Build linux-x64 (native)
+ CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
+ go build -tags sqlite_fts5 -o mind-map-linux-x64 ./cmd/mind-map/
+
+ # Build linux-arm64 (cross-compile)
+ CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc \
+ go build -tags sqlite_fts5 -o mind-map-linux-arm64 ./cmd/mind-map/
+
+ # Package as tarballs
+ tar czf mind-map-linux-x64.tar.gz mind-map-linux-x64
+ tar czf mind-map-linux-arm64.tar.gz mind-map-linux-arm64
+
+ - name: Upload linux-x64
+ uses: softprops/action-gh-release@v3
+ with:
+ files: mind-map-linux-x64.tar.gz
+
+ - name: Upload linux-arm64
+ uses: softprops/action-gh-release@v3
+ with:
+ files: mind-map-linux-arm64.tar.gz
+
+ build-macos:
+ name: Build ${{ matrix.artifact }}
+ runs-on: macos-latest
+ strategy:
+ matrix:
+ include:
+ - target_os: darwin
+ target_arch: amd64
+ artifact: mind-map-darwin-x64
+ - target_os: darwin
+ target_arch: arm64
+ artifact: mind-map-darwin-arm64
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-go@v5
+ with:
+ go-version: '1.26'
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 'lts/*'
+
+ - name: Build webui
+ run: cd webui && npm install && npm run build
+
+ - name: Build
+ env:
+ CGO_ENABLED: '1'
+ GOOS: ${{ matrix.target_os }}
+ GOARCH: ${{ matrix.target_arch }}
+ run: go build -tags sqlite_fts5 -o ${{ matrix.artifact }} ./cmd/mind-map/
+
+ - name: Codesign (ad-hoc)
+ run: codesign -s - ${{ matrix.artifact }} 2>/dev/null || true
+
+ - name: Package binary
+ run: tar czf ${{ matrix.artifact }}.tar.gz ${{ matrix.artifact }}
+
+ - name: Upload release asset
+ uses: softprops/action-gh-release@v3
+ with:
+ files: ${{ matrix.artifact }}.tar.gz
+
+ upload-install-script:
+ name: Upload install scripts
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - name: Upload install.sh and install.ps1
+ uses: softprops/action-gh-release@v3
+ with:
+ files: |
+ install.sh
+ install.ps1
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d790a2a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+# Build artifacts
+/mind-map
+
+# Wiki database files
+.mind-map.db
+.mind-map.db-shm
+.mind-map.db-wal
+
+# WebUI build output
+webui/dist/
+webui/node_modules/
+
+# VS Code cache
+.vscode/cache/
+__debug_bin*
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..1c7da2a
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,78 @@
+{
+ "version": "0.2.0",
+ "compounds": [
+ {
+ "name": "mind-map + WebUI",
+ "configurations": [
+ "mind-map Server",
+ "WebUI"
+ ],
+ "stopAll": true
+ }
+ ],
+ "configurations": [
+ {
+ "name": "mind-map Server",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${workspaceFolder}/cmd/mind-map",
+ "args": ["serve", "--addr", ":8080", "--dir", "${workspaceFolder}/testdata", "--webui", "${workspaceFolder}/webui/dist"],
+ "buildFlags": "-tags=sqlite_fts5",
+ "env": {
+ "CGO_ENABLED": "1"
+ },
+ "preLaunchTask": "build-webui"
+ },
+ {
+ "name": "mind-map Server (no WebUI)",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${workspaceFolder}/cmd/mind-map",
+ "args": ["serve", "--addr", ":8080", "--dir", "${workspaceFolder}/testdata", "--webui", "${workspaceFolder}/webui/dist"],
+ "buildFlags": "-tags=sqlite_fts5",
+ "env": {
+ "CGO_ENABLED": "1"
+ }
+ },
+ {
+ "name": "mind-map (stdio)",
+ "type": "go",
+ "request": "launch",
+ "mode": "auto",
+ "program": "${workspaceFolder}/cmd/mind-map",
+ "args": ["serve", "--stdio", "--dir", "${workspaceFolder}/testdata"],
+ "buildFlags": "-tags=sqlite_fts5",
+ "env": {
+ "CGO_ENABLED": "1"
+ }
+ },
+ {
+ "name": "WebUI",
+ "type": "chrome",
+ "request": "launch",
+ "browserLaunchLocation": "ui",
+ "runtimeExecutable": "stable",
+ "url": "http://localhost:8080",
+ "webRoot": "${workspaceFolder}/webui",
+ "preLaunchTask": "waitForServer",
+ "userDataDir": "${workspaceFolder}/.vscode/cache",
+ "sourceMaps": true,
+ "outFiles": [
+ "${workspaceFolder}/webui/dist/**/*.js"
+ ]
+ },
+ {
+ "name": "Test (all)",
+ "type": "go",
+ "request": "launch",
+ "mode": "test",
+ "program": "${workspaceFolder}/internal/wiki",
+ "buildFlags": "-tags=sqlite_fts5",
+ "env": {
+ "CGO_ENABLED": "1"
+ }
+ }
+ ]
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..52df030
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,62 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build-webui",
+ "type": "npm",
+ "script": "build",
+ "path": "webui",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": [],
+ "detail": "Build WebUI with webpack"
+ },
+ {
+ "label": "watch-webui",
+ "type": "npm",
+ "script": "dev",
+ "path": "webui",
+ "group": {
+ "kind": "build",
+ "isDefault": false
+ },
+ "isBackground": true,
+ "problemMatcher": [],
+ "detail": "Watch WebUI with webpack"
+ },
+ {
+ "label": "build-go",
+ "type": "shell",
+ "command": "CGO_ENABLED=1 go build -tags sqlite_fts5 -o ${workspaceFolder}/mind-map ./cmd/mind-map/",
+ "group": "build",
+ "problemMatcher": ["$go"],
+ "detail": "Build Go binary"
+ },
+ {
+ "label": "test",
+ "type": "shell",
+ "command": "CGO_ENABLED=1 go test -v -tags sqlite_fts5 ./...",
+ "group": {
+ "kind": "test",
+ "isDefault": true
+ },
+ "problemMatcher": ["$go"]
+ },
+ {
+ "label": "waitForServer",
+ "type": "shell",
+ "command": "while ! nc -z localhost 8080; do sleep 1; done",
+ "group": "none",
+ "dependsOn": ["build-webui"],
+ "problemMatcher": {
+ "base": "$tsc",
+ "background": {
+ "beginsPattern": ".*",
+ "endsPattern": ".*"
+ }
+ }
+ }
+ ]
+}
diff --git a/README.md b/README.md
index 0b46de7..a923b26 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,200 @@
# mind-map
-A wiki-style knowledgebase for AI agents
+
+[](https://github.com/aniongithub/mind-map/actions/workflows/ci.yml)
+
+**A wiki for AI agents — and humans too.**
+
+`mind-map` is a wiki engine that stores pages as plain markdown files, indexes them with SQLite FTS5, and exposes everything via MCP over HTTP/SSE. AI agents and humans connect to the same server using the same protocol. One binary, zero runtime dependencies.
+
+## The Problem
+
+AI agents need persistent, structured memory. Today that means:
+
+- 🔴 **Desktop apps** — tools like Tolaria require Node.js + Rust + WebKit + a display server just to give agents a knowledge base
+- 🔴 **Single-user** — stdio MCP is one agent, one pipe, that's it
+- 🔴 **No web access** — the knowledge is locked in a desktop app only the local user can see
+- 🔴 **Can't deploy headless** — needs a GUI environment even when no human is looking
+
+## The Solution
+
+`mind-map` is a **server**, not an app. It runs anywhere — your laptop, a container or a cloud VM.
+
+1. **One protocol** — MCP over HTTP/SSE. The web UI and AI agents are both MCP clients
+2. **One binary** — Go, statically compiled, `curl | bash` to install
+3. **Plain markdown** — pages are `.md` files with YAML frontmatter. Git-friendly, portable, yours
+4. **Multi-agent** — HTTP/SSE means any number of agents can connect simultaneously
+5. **Built-in web UI** — browse, search, and edit the wiki from any browser
+
+```
+Agent: "What do we know about authentication?"
+ → search_pages("authentication")
+ → get_page("architecture/auth")
+ → ✅ Full page with frontmatter, links, and backlinks
+```
+
+## Quick Install
+
+### Linux / macOS
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/aniongithub/mind-map/main/install.sh | bash
+```
+
+### Windows (via WSL)
+
+```powershell
+Invoke-RestMethod https://github.com/aniongithub/mind-map/releases/latest/download/install.ps1 | Invoke-Expression
+```
+
+> **How it works:** The binary runs inside WSL; MCP clients on Windows launch it via `wsl ~/.local/bin/mind-map serve --stdio`. WSL 2 is required — install it with `wsl --install` if you haven't already.
+
+Binaries available for **linux-x64**, **linux-arm64**, **darwin-x64**, and **darwin-arm64**.
+
+## Architecture
+
+```mermaid
+graph TD
+ A[Preact Web App] -->|MCP over HTTP/SSE| B
+ C[AI Agent] -->|MCP over HTTP/SSE| B
+ D[AI Agent] -->|MCP over stdio| B
+
+ subgraph "mind-map serve"
+ B[MCP Server] --> E[Wiki Engine]
+ E --> F[SQLite FTS5]
+ end
+
+ E -->|read/write| G[Markdown Files]
+```
+
+The web UI is a static Preact app served from the same binary. It connects to the MCP SSE endpoint at `/mcp` — the same endpoint AI agents use. There is no separate REST API.
+
+## Two Modes, One Server
+
+| Mode | Command | Use case |
+|------|---------|----------|
+| **HTTP/SSE** (default) | `mind-map serve --dir ~/wiki` | Web UI + multiple agents |
+| **stdio** | `mind-map serve --stdio --dir ~/wiki` | Single agent (Copilot, Claude Desktop, Cursor) |
+
+Both modes use the same wiki engine, same MCP tools, same code path. The only difference is the transport.
+
+## MCP Tools (8 total)
+
+| Tool | Description |
+|------|-------------|
+| `search_pages` | Full-text search across page titles and content (SQLite FTS5) |
+| `get_wiki_context` | Wiki overview — page count, top-level directories, recent pages |
+| `get_page` | Read a page with parsed frontmatter, body, outgoing links, and backlinks |
+| `create_page` | Create a new page (markdown with optional YAML frontmatter) |
+| `update_page` | Update an existing page's content |
+| `delete_page` | Delete a page from the wiki and search index |
+| `list_pages` | List pages, optionally filtered by path prefix |
+| `get_backlinks` | Get all pages that link to a given page |
+
+## Wiki Features
+
+- **YAML frontmatter** — structured metadata on every page (`title`, `type`, `status`, custom fields)
+- **Wikilinks** — `[[target]]` and `[[display|target]]` syntax, resolved to clickable links
+- **Backlink index** — every page knows what links to it
+- **Full-text search** — SQLite FTS5 with ranked results and snippets
+- **Concurrent access** — `sync.RWMutex` for safe multi-agent reads and writes
+
+## Web UI
+
+The built-in web UI is a metro-inspired, chromeless Preact app:
+
+- Sidebar with page list and search
+- Markdown rendering with wikilinks as clickable links
+- Backlinks section on every page
+- Edit mode with raw markdown editor
+- Dark / light theme toggle
+
+The web UI speaks MCP — it's an MCP client, not a separate interface. If an agent creates a page, it appears in the browser. If you edit in the browser, the agent sees the change.
+
+## MCP Server Configuration
+
+### Linux / macOS (stdio)
+
+```json
+{
+ "mcpServers": {
+ "mind-map": {
+ "command": "mind-map",
+ "args": ["serve", "--stdio", "--dir", "~/wiki"]
+ }
+ }
+}
+```
+
+### Windows (WSL bridge)
+
+```json
+{
+ "mcpServers": {
+ "mind-map": {
+ "command": "wsl",
+ "args": ["~/.local/bin/mind-map", "serve", "--stdio", "--dir", "~/wiki"]
+ }
+ }
+}
+```
+
+## Page Format
+
+Pages are plain markdown files with optional YAML frontmatter:
+
+```markdown
+---
+title: Authentication Architecture
+type: design-doc
+status: approved
+---
+# Authentication Architecture
+
+We use JWT tokens for API auth. See [[api/tokens]] for implementation.
+
+Related: [[security/threat-model]], [[api/rate-limiting]]
+```
+
+The wiki engine extracts:
+- **Title** from frontmatter `title:`, first `# heading`, or filename
+- **Frontmatter** as structured key-value metadata
+- **Wikilinks** (`[[target]]`) as outgoing links → stored in the backlink index
+
+## Development
+
+Development happens inside a [dev container](https://containers.dev/) — a reproducible, containerized environment defined by `.devcontainer/devcontainer.json`. This means no local Go or Node install required; everything runs in the container.
+
+You can manage the devcontainer with VS Code, [devcontainer-mcp](https://www.anionline.me/devcontainer-mcp/), or the [devcontainer CLI](https://github.com/devcontainers/cli).
+
+### VS Code (recommended)
+
+Open the repo in VS Code — it will prompt to reopen in the devcontainer. Or clone directly into one:
+
+> `Ctrl+Shift+P` → **Dev Containers: Clone Repository in Container Volume**
+
+Once inside, everything is ready:
+
+- **`Ctrl+Shift+B`** — build webui (default build task)
+- **`F5`** with **`mind-map + WebUI`** — starts the Go server + opens Chrome
+- **`watch-webui`** task — webpack watch for live reload
+
+### devcontainer-mcp
+
+If you use AI coding agents, [devcontainer-mcp](https://www.anionline.me/devcontainer-mcp/) lets the agent spin up and work inside the container directly — no manual setup.
+
+### CLI
+
+```bash
+devcontainer up --workspace-folder .
+devcontainer exec --workspace-folder . bash -c "cd webui && npm install && npm run build"
+devcontainer exec --workspace-folder . bash -c "CGO_ENABLED=1 go test -tags sqlite_fts5 ./..."
+```
+
+### CI/CD
+
+- **Pull Requests** — builds webui, runs `go vet`, `go test` in the devcontainer
+- **Releases** — creating a GitHub release cross-compiles for all 4 platforms and uploads binaries + install scripts
+
+## License
+
+[MIT](LICENSE)
diff --git a/cmd/mind-map/main.go b/cmd/mind-map/main.go
new file mode 100644
index 0000000..131b7d6
--- /dev/null
+++ b/cmd/mind-map/main.go
@@ -0,0 +1,104 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+ "os/signal"
+
+ "github.com/aniongithub/mind-map/internal/wiki"
+ mindmcp "github.com/aniongithub/mind-map/internal/mcp"
+ "github.com/modelcontextprotocol/go-sdk/mcp"
+ "github.com/spf13/cobra"
+)
+
+var rootCmd = &cobra.Command{
+ Use: "mind-map",
+ Short: "A wiki engine with MCP interface for AI agents",
+ Long: "mind-map is a wiki that stores pages as markdown files, indexes them with SQLite FTS5, and exposes everything via MCP over HTTP/SSE. AI agents and humans use the same protocol.",
+}
+
+var serveCmd = &cobra.Command{
+ Use: "serve",
+ Short: "Start the mind-map server",
+ Long: "Starts the MCP server. Use --stdio for single-agent stdio mode, or --addr for HTTP/SSE mode (default).",
+ RunE: runServe,
+}
+
+func init() {
+ serveCmd.Flags().StringP("dir", "d", ".", "Path to the wiki directory")
+ serveCmd.Flags().StringP("addr", "a", ":8080", "Address to listen on (HTTP/SSE mode)")
+ serveCmd.Flags().String("webui", "webui/dist", "Path to the webui dist directory")
+ serveCmd.Flags().Bool("stdio", false, "Run in stdio mode (single agent, for MCP client config)")
+ rootCmd.AddCommand(serveCmd)
+}
+
+func runServe(cmd *cobra.Command, args []string) error {
+ dir, _ := cmd.Flags().GetString("dir")
+ useStdio, _ := cmd.Flags().GetBool("stdio")
+
+ // Open the wiki
+ w, err := wiki.Open(dir)
+ if err != nil {
+ return fmt.Errorf("open wiki: %w", err)
+ }
+ defer w.Close()
+
+ // Create MCP server
+ s := mindmcp.NewServer(w)
+
+ if useStdio {
+ fmt.Fprintln(os.Stderr, "mind-map MCP server (stdio mode)")
+ fmt.Fprintf(os.Stderr, "Wiki: %s\n", w.Root())
+ return s.MCPServer().Run(cmd.Context(), &mcp.StdioTransport{})
+ }
+
+ // HTTP/SSE mode
+ addr, _ := cmd.Flags().GetString("addr")
+
+ sseHandler := mcp.NewSSEHandler(func(r *http.Request) *mcp.Server {
+ return s.MCPServer()
+ }, nil)
+
+ mux := http.NewServeMux()
+ mux.Handle("/mcp", sseHandler)
+
+ // Serve the webui
+ webDir, _ := cmd.Flags().GetString("webui")
+ if _, err := os.Stat(webDir); err == nil {
+ mux.Handle("/", http.FileServer(http.Dir(webDir)))
+ } else {
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ fmt.Fprint(w, `
+ mind-map
WebUI not built. Run npm run build in webui/
+ `)
+ })
+ }
+
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
+ defer stop()
+
+ server := &http.Server{Addr: addr, Handler: mux}
+
+ fmt.Fprintf(os.Stderr, "mind-map server on %s (wiki: %s)\n", addr, w.Root())
+ fmt.Fprintf(os.Stderr, "MCP SSE endpoint: http://localhost%s/mcp\n", addr)
+
+ go func() {
+ <-ctx.Done()
+ server.Close()
+ }()
+
+ if err := server.ListenAndServe(); err != http.ErrServerClosed {
+ return err
+ }
+ return nil
+}
+
+func main() {
+ if err := rootCmd.Execute(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..83f1957
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,23 @@
+module github.com/aniongithub/mind-map
+
+go 1.26.2
+
+require (
+ github.com/mattn/go-sqlite3 v1.14.42
+ github.com/modelcontextprotocol/go-sdk v1.5.0
+ github.com/spf13/cobra v1.10.2
+ github.com/yuin/goldmark v1.8.2
+ github.com/yuin/goldmark-meta v1.1.0
+)
+
+require (
+ github.com/google/jsonschema-go v0.4.2 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/segmentio/asm v1.1.3 // indirect
+ github.com/segmentio/encoding v0.5.4 // indirect
+ github.com/spf13/pflag v1.0.9 // indirect
+ github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+ golang.org/x/oauth2 v0.35.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+ gopkg.in/yaml.v2 v2.3.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..22c7f7a
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,39 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
+github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
+github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
+github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
+github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU=
+github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
+github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
+github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
+github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
+github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
+github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
+github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
+golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
+golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/install.ps1 b/install.ps1
new file mode 100644
index 0000000..f7e31ea
--- /dev/null
+++ b/install.ps1
@@ -0,0 +1,180 @@
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+ Windows installer for mind-map (via WSL).
+.DESCRIPTION
+ Installs the mind-map Linux binary inside WSL and configures
+ Windows-side MCP clients to use the WSL bridge ("command": "wsl").
+.EXAMPLE
+ irm https://github.com/aniongithub/mind-map/releases/latest/download/install.ps1 | iex
+#>
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+$Repo = "aniongithub/mind-map"
+$WslBinaryPath = "~/.local/bin/mind-map"
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+function Write-Step { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Cyan }
+function Write-Ok { param([string]$Message) Write-Host " $([char]0x2713) $Message" -ForegroundColor Green }
+function Write-Warn { param([string]$Message) Write-Host " $([char]0x26A0) $Message" -ForegroundColor Yellow }
+function Write-Fail { param([string]$Message) Write-Host " $([char]0x2717) $Message" -ForegroundColor Red }
+
+# ---------------------------------------------------------------------------
+# 1. Verify WSL is available
+# ---------------------------------------------------------------------------
+
+Write-Step "Checking for WSL..."
+
+try {
+ $wslStatus = wsl --status 2>&1
+ if ($LASTEXITCODE -ne 0) { throw "WSL returned non-zero exit code" }
+ Write-Ok "WSL is available"
+} catch {
+ Write-Host ""
+ Write-Host "Error: WSL (Windows Subsystem for Linux) is required but not found." -ForegroundColor Red
+ Write-Host ""
+ Write-Host "Install WSL with: wsl --install" -ForegroundColor Yellow
+ Write-Host "Then restart your computer and run this script again."
+ Write-Host "More info: https://learn.microsoft.com/en-us/windows/wsl/install"
+ exit 1
+}
+
+# Find usable WSL distros (skip docker-desktop* distros which are minimal)
+$WslDistro = $null
+$distroLines = (wsl -l -q 2>&1) -replace "`0", "" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
+$usableDistros = @($distroLines | Where-Object { $_ -notmatch '^docker-desktop' })
+
+if ($usableDistros.Count -eq 0) {
+ Write-Host ""
+ Write-Host "Error: No usable WSL distro found (docker-desktop is not supported)." -ForegroundColor Red
+ Write-Host ""
+ Write-Host "Install a Linux distro with: wsl --install Ubuntu" -ForegroundColor Yellow
+ exit 1
+} elseif ($usableDistros.Count -eq 1) {
+ $WslDistro = $usableDistros[0]
+} else {
+ Write-Host ""
+ Write-Host "Available WSL distros:" -ForegroundColor Cyan
+ for ($i = 0; $i -lt $usableDistros.Count; $i++) {
+ Write-Host " [$($i + 1)] $($usableDistros[$i])"
+ }
+ Write-Host ""
+ $choice = Read-Host "Select a distro (1-$($usableDistros.Count))"
+ $idx = [int]$choice - 1
+ if ($idx -lt 0 -or $idx -ge $usableDistros.Count) {
+ Write-Host "Invalid selection." -ForegroundColor Red
+ exit 1
+ }
+ $WslDistro = $usableDistros[$idx]
+}
+
+Write-Ok "Using WSL distro: $WslDistro"
+
+# ---------------------------------------------------------------------------
+# 2. Install binary inside WSL (reuse install.sh)
+# ---------------------------------------------------------------------------
+
+Write-Step "Installing mind-map binary inside WSL..."
+
+$installUrl = "https://raw.githubusercontent.com/$Repo/main/install.sh"
+$wslResult = wsl -d $WslDistro bash -c "curl -fsSL '$installUrl' | bash -s -- --skip-mcp-config" 2>&1
+$wslResult | ForEach-Object { Write-Host " $_" }
+
+if ($LASTEXITCODE -ne 0) {
+ Write-Host ""
+ Write-Host "Error: Binary installation inside WSL failed." -ForegroundColor Red
+ Write-Host "Try running manually in WSL: curl -fsSL $installUrl | bash"
+ exit 1
+}
+
+# Verify the binary works
+$versionCheck = wsl -d $WslDistro bash -lc "$WslBinaryPath --help" 2>&1
+if ($LASTEXITCODE -eq 0) {
+ Write-Ok "Installed: mind-map"
+} else {
+ Write-Warn "Binary installed but could not verify"
+}
+
+# ---------------------------------------------------------------------------
+# 3. Configure Windows-side MCP clients (with WSL bridge)
+# ---------------------------------------------------------------------------
+
+Write-Step "Configuring MCP clients..."
+
+$mcpServerEntry = @{
+ command = "wsl"
+ args = @($WslBinaryPath, "serve", "--stdio")
+}
+
+function Set-McpConfig {
+ param(
+ [string]$ConfigPath,
+ [string]$ClientName
+ )
+
+ try {
+ if (Test-Path $ConfigPath) {
+ $content = Get-Content -Raw $ConfigPath | ConvertFrom-Json
+ if (-not $content.mcpServers) {
+ $content | Add-Member -NotePropertyName "mcpServers" -NotePropertyValue ([PSCustomObject]@{})
+ }
+ if ($content.mcpServers.PSObject.Properties.Name -contains "mind-map") {
+ Write-Ok "$ClientName — already configured"
+ return
+ }
+ $content.mcpServers | Add-Member -NotePropertyName "mind-map" -NotePropertyValue ([PSCustomObject]$mcpServerEntry)
+ $content | ConvertTo-Json -Depth 10 | Set-Content $ConfigPath -Encoding UTF8
+ Write-Ok "$ClientName — added to $ConfigPath"
+ } else {
+ $dir = Split-Path $ConfigPath -Parent
+ if ($dir) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
+ $config = [PSCustomObject]@{
+ mcpServers = [PSCustomObject]@{
+ "mind-map" = [PSCustomObject]$mcpServerEntry
+ }
+ }
+ $config | ConvertTo-Json -Depth 10 | Set-Content $ConfigPath -Encoding UTF8
+ Write-Ok "$ClientName — created $ConfigPath"
+ }
+ } catch {
+ Write-Warn "$ClientName — could not update $ConfigPath"
+ }
+}
+
+# Claude Code
+Set-McpConfig "$env:USERPROFILE\.claude.json" "Claude Code"
+
+# GitHub Copilot (if .copilot dir exists)
+if (Test-Path "$env:USERPROFILE\.copilot") {
+ Set-McpConfig "$env:USERPROFILE\.copilot\mcp-config.json" "GitHub Copilot"
+}
+
+# VS Code (if config dir exists)
+$vscodeDir = "$env:APPDATA\Code\User"
+if (Test-Path $vscodeDir) {
+ Set-McpConfig "$vscodeDir\mcp.json" "VS Code"
+}
+
+# Cursor (if installed)
+if (Test-Path "$env:USERPROFILE\.cursor") {
+ Set-McpConfig "$env:USERPROFILE\.cursor\mcp.json" "Cursor"
+}
+
+# ---------------------------------------------------------------------------
+# Done
+# ---------------------------------------------------------------------------
+
+Write-Host ""
+Write-Host "Done! mind-map is ready to use." -ForegroundColor Green
+Write-Host ""
+Write-Host "MCP clients are configured to launch the server via WSL:" -ForegroundColor DarkGray
+Write-Host " command: wsl" -ForegroundColor DarkGray
+Write-Host " args: [$WslBinaryPath, serve, --stdio]" -ForegroundColor DarkGray
+Write-Host ""
+Write-Host "Start the wiki server (from WSL):" -ForegroundColor DarkGray
+Write-Host " mind-map serve --dir ~/wiki" -ForegroundColor DarkGray
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..6a5011d
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,187 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# mind-map installer
+# Downloads the latest release binary.
+#
+# Usage:
+# curl -fsSL https://raw.githubusercontent.com/aniongithub/mind-map/main/install.sh | bash
+# curl -fsSL ... | bash -s -- --install-dir /usr/local/bin
+
+REPO="aniongithub/mind-map"
+INSTALL_DIR="${HOME}/.local/bin"
+SKIP_MCP_CONFIG=false
+
+# Parse args
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --install-dir) INSTALL_DIR="$2"; shift 2 ;;
+ --skip-mcp-config) SKIP_MCP_CONFIG=true; shift ;;
+ --help|-h)
+ echo "Usage: install.sh [--install-dir DIR] [--skip-mcp-config]"
+ echo " --install-dir Installation directory (default: ~/.local/bin)"
+ echo " --skip-mcp-config Skip MCP client configuration (used by install.ps1)"
+ exit 0
+ ;;
+ *) echo "Unknown option: $1"; exit 1 ;;
+ esac
+done
+
+# Detect OS and architecture
+detect_platform() {
+ local os arch
+ os="$(uname -s)"
+ arch="$(uname -m)"
+
+ case "$os" in
+ Linux) os="linux" ;;
+ Darwin) os="darwin" ;;
+ *) echo "Error: Unsupported OS: $os"; exit 1 ;;
+ esac
+
+ case "$arch" in
+ x86_64|amd64) arch="x64" ;;
+ aarch64|arm64) arch="arm64" ;;
+ *) echo "Error: Unsupported architecture: $arch"; exit 1 ;;
+ esac
+
+ echo "${os}-${arch}"
+}
+
+# Get latest release tag from GitHub
+get_latest_version() {
+ curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
+ | grep '"tag_name"' \
+ | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/'
+}
+
+PLATFORM="$(detect_platform)"
+echo "==> Detected platform: ${PLATFORM}"
+
+VERSION="$(get_latest_version)"
+if [[ -z "$VERSION" ]]; then
+ echo "Error: Could not determine latest release version."
+ echo "Check: https://github.com/${REPO}/releases"
+ exit 1
+fi
+echo "==> Latest version: ${VERSION}"
+
+TARBALL_NAME="mind-map-${PLATFORM}.tar.gz"
+DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${TARBALL_NAME}"
+
+# Create install directory
+mkdir -p "$INSTALL_DIR"
+
+echo "==> Downloading ${TARBALL_NAME}..."
+curl -fsSL "$DOWNLOAD_URL" | tar xz -C "${INSTALL_DIR}"
+chmod +x "${INSTALL_DIR}/mind-map"
+
+# Rename platform-specific binary to just "mind-map"
+if [[ -f "${INSTALL_DIR}/mind-map-${PLATFORM}" ]]; then
+ mv "${INSTALL_DIR}/mind-map-${PLATFORM}" "${INSTALL_DIR}/mind-map"
+fi
+
+# macOS: ad-hoc codesign to avoid Gatekeeper "Killed: 9"
+if [[ "$(uname -s)" == "Darwin" ]]; then
+ codesign -s - "${INSTALL_DIR}/mind-map" 2>/dev/null && \
+ echo "==> Codesigned binary for macOS" || true
+fi
+
+echo "==> Installed mind-map to ${INSTALL_DIR}/mind-map"
+
+# Verify
+if "${INSTALL_DIR}/mind-map" --help >/dev/null 2>&1; then
+ echo "==> mind-map is working"
+else
+ echo "Warning: Binary downloaded but failed to run. Check platform compatibility."
+fi
+
+# Check PATH
+if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
+ echo ""
+ echo "Note: ${INSTALL_DIR} is not in your PATH. Add it with:"
+ echo " export PATH=\"${INSTALL_DIR}:\$PATH\""
+fi
+
+# ---------------------------------------------------------------------------
+# Auto-configure MCP clients (skipped when called from install.ps1)
+# ---------------------------------------------------------------------------
+
+if [ "$SKIP_MCP_CONFIG" = true ]; then
+ echo ""
+ echo "==> Skipping MCP client configuration (--skip-mcp-config)"
+ echo ""
+ echo "Done! mind-map binary is installed."
+ exit 0
+fi
+
+# Configure MCP clients
+configure_mcp_client() {
+ local config_file="$1"
+ local client_name="$2"
+
+ if [ ! -f "$config_file" ]; then
+ mkdir -p "$(dirname "$config_file")"
+ cat > "$config_file" << MCPEOF
+{
+ "mcpServers": {
+ "mind-map": {
+ "command": "${INSTALL_DIR}/mind-map",
+ "args": ["serve", "--stdio"]
+ }
+ }
+}
+MCPEOF
+ echo " ✓ ${client_name} — created ${config_file}"
+ elif command -v python3 >/dev/null 2>&1; then
+ python3 -c "
+import json
+path = '${config_file}'
+with open(path) as f:
+ data = json.load(f)
+servers = data.setdefault('mcpServers', {})
+if 'mind-map' not in servers:
+ servers['mind-map'] = {'command': '${INSTALL_DIR}/mind-map', 'args': ['serve', '--stdio']}
+ with open(path, 'w') as f:
+ json.dump(data, f, indent=2)
+ print(' ✓ ${client_name} — added to ${config_file}')
+else:
+ print(' ✓ ${client_name} — already configured')
+" 2>/dev/null || echo " ⚠ ${client_name} — could not update ${config_file}"
+ else
+ echo " ⚠ ${client_name} — exists but python3 not available to merge"
+ fi
+}
+
+echo ""
+echo "==> Configuring MCP clients..."
+
+# GitHub Copilot CLI
+if [ -d "${HOME}/.copilot" ]; then
+ configure_mcp_client "${HOME}/.copilot/mcp-config.json" "GitHub Copilot"
+fi
+
+# VS Code
+if [[ "$(uname -s)" == "Darwin" ]]; then
+ VSCODE_DIR="${HOME}/Library/Application Support/Code/User"
+else
+ VSCODE_DIR="${HOME}/.config/Code/User"
+fi
+if [ -d "$VSCODE_DIR" ]; then
+ configure_mcp_client "${VSCODE_DIR}/mcp.json" "VS Code"
+fi
+
+# Cursor
+if [ -d "${HOME}/.cursor" ]; then
+ configure_mcp_client "${HOME}/.cursor/mcp.json" "Cursor"
+fi
+
+# Claude Code
+configure_mcp_client "${HOME}/.claude.json" "Claude Code"
+
+echo ""
+echo "Done! mind-map is ready."
+echo ""
+echo " Start the wiki server: mind-map serve --dir ~/wiki"
+echo " Start as MCP server: mind-map serve --stdio --dir ~/wiki"
+echo ""
diff --git a/internal/mcp/server.go b/internal/mcp/server.go
new file mode 100644
index 0000000..69f13f7
--- /dev/null
+++ b/internal/mcp/server.go
@@ -0,0 +1,190 @@
+// Package mcp implements MCP tool definitions that wrap the wiki engine.
+// Each tool is a thin adapter from MCP request/response to wiki operations.
+package mcp
+
+import (
+ "context"
+ "encoding/json"
+
+ "github.com/aniongithub/mind-map/internal/wiki"
+ "github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+// Server wraps a Wiki and exposes it as MCP tools.
+type Server struct {
+ wiki *wiki.Wiki
+ server *mcp.Server
+}
+
+// NewServer creates an MCP server backed by the given wiki.
+func NewServer(w *wiki.Wiki) *Server {
+ s := &Server{
+ wiki: w,
+ server: mcp.NewServer(&mcp.Implementation{
+ Name: "mind-map",
+ Version: "0.1.0",
+ }, nil),
+ }
+ s.registerTools()
+ return s
+}
+
+// MCPServer returns the underlying mcp.Server for transport binding.
+func (s *Server) MCPServer() *mcp.Server {
+ return s.server
+}
+
+func (s *Server) registerTools() {
+ mcp.AddTool(s.server, &mcp.Tool{
+ Name: "search_pages",
+ Description: "Full-text search across wiki pages by title or content. Returns matching paths, titles, and snippets.",
+ }, s.searchPages)
+
+ mcp.AddTool(s.server, &mcp.Tool{
+ Name: "get_wiki_context",
+ Description: "Get wiki orientation: page count, top-level directories, and 20 most recently modified pages.",
+ }, s.getWikiContext)
+
+ mcp.AddTool(s.server, &mcp.Tool{
+ Name: "get_page",
+ Description: "Read a wiki page with parsed frontmatter, body, outgoing links, and backlinks.",
+ }, s.getPage)
+
+ mcp.AddTool(s.server, &mcp.Tool{
+ Name: "create_page",
+ Description: "Create a new wiki page. Content should be markdown, optionally with YAML frontmatter.",
+ }, s.createPage)
+
+ mcp.AddTool(s.server, &mcp.Tool{
+ Name: "update_page",
+ Description: "Update an existing wiki page's content.",
+ }, s.updatePage)
+
+ mcp.AddTool(s.server, &mcp.Tool{
+ Name: "delete_page",
+ Description: "Delete a wiki page.",
+ }, s.deletePage)
+
+ mcp.AddTool(s.server, &mcp.Tool{
+ Name: "list_pages",
+ Description: "List wiki pages, optionally filtered by a path prefix.",
+ }, s.listPages)
+
+ mcp.AddTool(s.server, &mcp.Tool{
+ Name: "get_backlinks",
+ Description: "Get all pages that link to the specified page.",
+ }, s.getBacklinks)
+}
+
+// --- Tool input types ---
+
+type searchInput struct {
+ Query string `json:"query" jsonschema:"search query string"`
+ Limit int `json:"limit,omitempty" jsonschema:"maximum results, default 20"`
+}
+
+type pagePathInput struct {
+ Path string `json:"path" jsonschema:"page path without .md extension, e.g. projects/mind-map"`
+}
+
+type createInput struct {
+ Path string `json:"path" jsonschema:"page path without .md extension"`
+ Content string `json:"content" jsonschema:"markdown content, optionally with YAML frontmatter"`
+}
+
+type updateInput struct {
+ Path string `json:"path" jsonschema:"page path without .md extension"`
+ Content string `json:"content" jsonschema:"new markdown content"`
+}
+
+type listInput struct {
+ Prefix string `json:"prefix,omitempty" jsonschema:"filter pages by path prefix"`
+}
+
+// --- Tool handlers ---
+
+func (s *Server) searchPages(_ context.Context, _ *mcp.CallToolRequest, input searchInput) (*mcp.CallToolResult, any, error) {
+ results, err := s.wiki.Search(input.Query, input.Limit)
+ if err != nil {
+ return nil, nil, err
+ }
+ return textResult(results)
+}
+
+func (s *Server) getWikiContext(_ context.Context, _ *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
+ ctx, err := s.wiki.Context()
+ if err != nil {
+ return nil, nil, err
+ }
+ return textResult(ctx)
+}
+
+func (s *Server) getPage(_ context.Context, _ *mcp.CallToolRequest, input pagePathInput) (*mcp.CallToolResult, any, error) {
+ page, err := s.wiki.GetPage(input.Path)
+ if err != nil {
+ return nil, nil, err
+ }
+ return textResult(page)
+}
+
+func (s *Server) createPage(_ context.Context, _ *mcp.CallToolRequest, input createInput) (*mcp.CallToolResult, any, error) {
+ if err := s.wiki.CreatePage(input.Path, input.Content); err != nil {
+ return nil, nil, err
+ }
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ &mcp.TextContent{Text: "Created page: " + input.Path},
+ },
+ }, nil, nil
+}
+
+func (s *Server) updatePage(_ context.Context, _ *mcp.CallToolRequest, input updateInput) (*mcp.CallToolResult, any, error) {
+ if err := s.wiki.UpdatePage(input.Path, input.Content); err != nil {
+ return nil, nil, err
+ }
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ &mcp.TextContent{Text: "Updated page: " + input.Path},
+ },
+ }, nil, nil
+}
+
+func (s *Server) deletePage(_ context.Context, _ *mcp.CallToolRequest, input pagePathInput) (*mcp.CallToolResult, any, error) {
+ if err := s.wiki.DeletePage(input.Path); err != nil {
+ return nil, nil, err
+ }
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ &mcp.TextContent{Text: "Deleted page: " + input.Path},
+ },
+ }, nil, nil
+}
+
+func (s *Server) listPages(_ context.Context, _ *mcp.CallToolRequest, input listInput) (*mcp.CallToolResult, any, error) {
+ pages, err := s.wiki.ListPages(input.Prefix)
+ if err != nil {
+ return nil, nil, err
+ }
+ return textResult(pages)
+}
+
+func (s *Server) getBacklinks(_ context.Context, _ *mcp.CallToolRequest, input pagePathInput) (*mcp.CallToolResult, any, error) {
+ backlinks, err := s.wiki.GetBacklinks(input.Path)
+ if err != nil {
+ return nil, nil, err
+ }
+ return textResult(backlinks)
+}
+
+// textResult marshals any value to JSON and returns it as an MCP text result.
+func textResult(v any) (*mcp.CallToolResult, any, error) {
+ data, err := json.MarshalIndent(v, "", " ")
+ if err != nil {
+ return nil, nil, err
+ }
+ return &mcp.CallToolResult{
+ Content: []mcp.Content{
+ &mcp.TextContent{Text: string(data)},
+ },
+ }, nil, nil
+}
diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go
new file mode 100644
index 0000000..1dc0835
--- /dev/null
+++ b/internal/mcp/server_test.go
@@ -0,0 +1,267 @@
+package mcp
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/aniongithub/mind-map/internal/wiki"
+ "github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+// setupTestServer creates a wiki with test pages and connects an MCP client.
+func setupTestServer(t *testing.T) *mcp.ClientSession {
+ t.Helper()
+
+ dir := t.TempDir()
+
+ writeTestFile(t, dir, "index.md", `---
+title: Home
+---
+# Welcome
+
+This is the home page. See [[projects/mind-map]] and [[people/alice]].
+`)
+ writeTestFile(t, dir, "projects/mind-map.md", `---
+title: mind-map
+type: project
+status: active
+---
+# mind-map
+
+A wiki engine for AI agents. Built with [[Go]].
+`)
+ writeTestFile(t, dir, "people/alice.md", `# Alice
+
+Alice works on [[projects/mind-map]].
+`)
+ writeTestFile(t, dir, "Go.md", `# Go
+
+A programming language.
+`)
+
+ w, err := wiki.Open(dir)
+ if err != nil {
+ t.Fatalf("Open wiki: %v", err)
+ }
+ t.Cleanup(func() { w.Close() })
+
+ s := NewServer(w)
+
+ client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "v0.0.1"}, nil)
+ ct, st := mcp.NewInMemoryTransports()
+
+ ctx := context.Background()
+ if _, err := s.MCPServer().Connect(ctx, st, nil); err != nil {
+ t.Fatalf("server connect: %v", err)
+ }
+ session, err := client.Connect(ctx, ct, nil)
+ if err != nil {
+ t.Fatalf("client connect: %v", err)
+ }
+ t.Cleanup(func() { session.Close() })
+
+ return session
+}
+
+func writeTestFile(t *testing.T, root, relPath, content string) {
+ t.Helper()
+ abs := filepath.Join(root, relPath)
+ os.MkdirAll(filepath.Dir(abs), 0o755)
+ if err := os.WriteFile(abs, []byte(content), 0o644); err != nil {
+ t.Fatalf("write %s: %v", relPath, err)
+ }
+}
+
+func callTool(t *testing.T, session *mcp.ClientSession, name string, args map[string]any) string {
+ t.Helper()
+ ctx := context.Background()
+ result, err := session.CallTool(ctx, &mcp.CallToolParams{
+ Name: name,
+ Arguments: args,
+ })
+ if err != nil {
+ t.Fatalf("CallTool(%s): %v", name, err)
+ }
+ if len(result.Content) == 0 {
+ t.Fatalf("CallTool(%s): empty content", name)
+ }
+ tc, ok := result.Content[0].(*mcp.TextContent)
+ if !ok {
+ t.Fatalf("CallTool(%s): expected TextContent, got %T", name, result.Content[0])
+ }
+ return tc.Text
+}
+
+func TestListTools(t *testing.T) {
+ session := setupTestServer(t)
+ ctx := context.Background()
+
+ var tools []mcp.Tool
+ for tool, err := range session.Tools(ctx, nil) {
+ if err != nil {
+ t.Fatalf("Tools: %v", err)
+ }
+ tools = append(tools, *tool)
+ }
+
+ expected := map[string]bool{
+ "search_pages": false,
+ "get_wiki_context": false,
+ "get_page": false,
+ "create_page": false,
+ "update_page": false,
+ "delete_page": false,
+ "list_pages": false,
+ "get_backlinks": false,
+ }
+ for _, tool := range tools {
+ if _, ok := expected[tool.Name]; ok {
+ expected[tool.Name] = true
+ }
+ }
+ for name, found := range expected {
+ if !found {
+ t.Errorf("tool %q not found", name)
+ }
+ }
+}
+
+func TestGetWikiContext(t *testing.T) {
+ session := setupTestServer(t)
+ text := callTool(t, session, "get_wiki_context", nil)
+
+ var ctx wiki.WikiContext
+ if err := json.Unmarshal([]byte(text), &ctx); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if ctx.PageCount != 4 {
+ t.Errorf("PageCount = %d, want 4", ctx.PageCount)
+ }
+}
+
+func TestGetPage(t *testing.T) {
+ session := setupTestServer(t)
+ text := callTool(t, session, "get_page", map[string]any{"path": "projects/mind-map"})
+
+ var page wiki.Page
+ if err := json.Unmarshal([]byte(text), &page); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if page.Title != "mind-map" {
+ t.Errorf("Title = %q, want %q", page.Title, "mind-map")
+ }
+ if page.Frontmatter["type"] != "project" {
+ t.Errorf("Frontmatter[type] = %v, want %q", page.Frontmatter["type"], "project")
+ }
+}
+
+func TestSearchPages(t *testing.T) {
+ session := setupTestServer(t)
+ text := callTool(t, session, "search_pages", map[string]any{"query": "wiki engine"})
+
+ var results []wiki.SearchResult
+ if err := json.Unmarshal([]byte(text), &results); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if len(results) == 0 {
+ t.Fatal("expected at least 1 result")
+ }
+ if results[0].Path != "projects/mind-map" {
+ t.Errorf("first result = %q, want %q", results[0].Path, "projects/mind-map")
+ }
+}
+
+func TestGetBacklinks(t *testing.T) {
+ session := setupTestServer(t)
+ text := callTool(t, session, "get_backlinks", map[string]any{"path": "projects/mind-map"})
+
+ var backlinks []string
+ if err := json.Unmarshal([]byte(text), &backlinks); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if len(backlinks) != 2 {
+ t.Errorf("backlinks = %v, want 2 entries (index, people/alice)", backlinks)
+ }
+}
+
+func TestListPages(t *testing.T) {
+ session := setupTestServer(t)
+
+ // All pages
+ text := callTool(t, session, "list_pages", map[string]any{})
+ var all []wiki.Page
+ if err := json.Unmarshal([]byte(text), &all); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if len(all) != 4 {
+ t.Errorf("all pages = %d, want 4", len(all))
+ }
+
+ // Filtered
+ text = callTool(t, session, "list_pages", map[string]any{"prefix": "projects"})
+ var filtered []wiki.Page
+ if err := json.Unmarshal([]byte(text), &filtered); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if len(filtered) != 1 {
+ t.Errorf("filtered pages = %d, want 1", len(filtered))
+ }
+}
+
+func TestCreatePage(t *testing.T) {
+ session := setupTestServer(t)
+
+ content := "---\ntitle: New Page\n---\n# New Page\n\nLinks to [[index]].\n"
+ callTool(t, session, "create_page", map[string]any{
+ "path": "new-page",
+ "content": content,
+ })
+
+ // Verify via get_page
+ text := callTool(t, session, "get_page", map[string]any{"path": "new-page"})
+ var page wiki.Page
+ if err := json.Unmarshal([]byte(text), &page); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if page.Title != "New Page" {
+ t.Errorf("Title = %q, want %q", page.Title, "New Page")
+ }
+}
+
+func TestUpdatePage(t *testing.T) {
+ session := setupTestServer(t)
+
+ newContent := "---\ntitle: Updated Home\n---\n# Updated\n\nNow links to [[Go]] only.\n"
+ callTool(t, session, "update_page", map[string]any{
+ "path": "index",
+ "content": newContent,
+ })
+
+ text := callTool(t, session, "get_page", map[string]any{"path": "index"})
+ var page wiki.Page
+ if err := json.Unmarshal([]byte(text), &page); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if page.Title != "Updated Home" {
+ t.Errorf("Title = %q, want %q", page.Title, "Updated Home")
+ }
+}
+
+func TestDeletePage(t *testing.T) {
+ session := setupTestServer(t)
+
+ callTool(t, session, "delete_page", map[string]any{"path": "Go"})
+
+ // Verify it's gone — get_page should return an error result
+ ctx := context.Background()
+ result, err := session.CallTool(ctx, &mcp.CallToolParams{
+ Name: "get_page",
+ Arguments: map[string]any{"path": "Go"},
+ })
+ if err == nil && !result.IsError {
+ t.Error("expected error after deleting page, got success")
+ }
+}
diff --git a/internal/wiki/index.go b/internal/wiki/index.go
new file mode 100644
index 0000000..c1f23d7
--- /dev/null
+++ b/internal/wiki/index.go
@@ -0,0 +1,165 @@
+package wiki
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// Reindex scans the wiki directory and rebuilds the entire index.
+func (w *Wiki) Reindex() error {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ tx, err := w.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ // Clear existing data
+ if _, err := tx.Exec("DELETE FROM links"); err != nil {
+ return err
+ }
+ if _, err := tx.Exec("DELETE FROM pages"); err != nil {
+ return err
+ }
+
+ // Walk the filesystem
+ err = filepath.WalkDir(w.root, func(path string, d os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+
+ // Skip hidden dirs and files
+ name := d.Name()
+ if strings.HasPrefix(name, ".") {
+ if d.IsDir() {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+
+ if d.IsDir() || !strings.HasSuffix(name, ".md") {
+ return nil
+ }
+
+ rel, err := filepath.Rel(w.root, path)
+ if err != nil {
+ return err
+ }
+ // Normalize to forward slashes, strip .md extension
+ pagePath := strings.TrimSuffix(filepath.ToSlash(rel), ".md")
+
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ return fmt.Errorf("read %s: %w", path, err)
+ }
+
+ info, err := d.Info()
+ if err != nil {
+ return err
+ }
+
+ parsed := parsePage(raw)
+ if parsed.title == "" {
+ parsed.title = filepath.Base(pagePath)
+ }
+
+ metaJSON, _ := json.Marshal(parsed.frontmatter)
+
+ _, err = tx.Exec(
+ "INSERT OR REPLACE INTO pages (path, title, body, meta, modified) VALUES (?, ?, ?, ?, ?)",
+ pagePath, parsed.title, parsed.body, string(metaJSON), info.ModTime().UTC().Format(time.RFC3339),
+ )
+ if err != nil {
+ return fmt.Errorf("index %s: %w", pagePath, err)
+ }
+
+ for _, target := range parsed.links {
+ _, err = tx.Exec(
+ "INSERT OR IGNORE INTO links (source, target) VALUES (?, ?)",
+ pagePath, target,
+ )
+ if err != nil {
+ return fmt.Errorf("index link %s->%s: %w", pagePath, target, err)
+ }
+ }
+
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ return tx.Commit()
+}
+
+// indexPage indexes a single page (after write/update).
+func (w *Wiki) indexPage(pagePath string) error {
+ absPath := filepath.Join(w.root, pagePath+".md")
+
+ raw, err := os.ReadFile(absPath)
+ if err != nil {
+ return fmt.Errorf("read %s: %w", absPath, err)
+ }
+
+ info, err := os.Stat(absPath)
+ if err != nil {
+ return err
+ }
+
+ parsed := parsePage(raw)
+ if parsed.title == "" {
+ parsed.title = filepath.Base(pagePath)
+ }
+
+ metaJSON, _ := json.Marshal(parsed.frontmatter)
+
+ tx, err := w.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ _, err = tx.Exec(
+ "INSERT OR REPLACE INTO pages (path, title, body, meta, modified) VALUES (?, ?, ?, ?, ?)",
+ pagePath, parsed.title, parsed.body, string(metaJSON), info.ModTime().UTC().Format(time.RFC3339),
+ )
+ if err != nil {
+ return err
+ }
+
+ // Rebuild links for this page
+ if _, err := tx.Exec("DELETE FROM links WHERE source = ?", pagePath); err != nil {
+ return err
+ }
+ for _, target := range parsed.links {
+ if _, err := tx.Exec("INSERT OR IGNORE INTO links (source, target) VALUES (?, ?)", pagePath, target); err != nil {
+ return err
+ }
+ }
+
+ return tx.Commit()
+}
+
+// removePageIndex removes a page from the index.
+func (w *Wiki) removePageIndex(pagePath string) error {
+ tx, err := w.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ if _, err := tx.Exec("DELETE FROM pages WHERE path = ?", pagePath); err != nil {
+ return err
+ }
+ if _, err := tx.Exec("DELETE FROM links WHERE source = ?", pagePath); err != nil {
+ return err
+ }
+
+ return tx.Commit()
+}
diff --git a/internal/wiki/pages.go b/internal/wiki/pages.go
new file mode 100644
index 0000000..6541c16
--- /dev/null
+++ b/internal/wiki/pages.go
@@ -0,0 +1,257 @@
+package wiki
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// GetPage retrieves a single page by path.
+func (w *Wiki) GetPage(pagePath string) (*Page, error) {
+ w.mu.RLock()
+ defer w.mu.RUnlock()
+
+ var title, body, metaStr, modified string
+ err := w.db.QueryRow(
+ "SELECT title, body, meta, modified FROM pages WHERE path = ?", pagePath,
+ ).Scan(&title, &body, &metaStr, &modified)
+ if err != nil {
+ return nil, fmt.Errorf("page not found: %s", pagePath)
+ }
+
+ var fm map[string]interface{}
+ json.Unmarshal([]byte(metaStr), &fm)
+
+ modTime, _ := time.Parse(time.RFC3339, modified)
+
+ links, _ := w.getLinks(pagePath)
+ backlinks, _ := w.getBacklinks(pagePath)
+
+ return &Page{
+ Path: pagePath,
+ Title: title,
+ Body: body,
+ Frontmatter: fm,
+ Links: links,
+ Backlinks: backlinks,
+ ModifiedAt: modTime,
+ }, nil
+}
+
+// ListPages returns all pages, optionally filtered by a prefix path.
+func (w *Wiki) ListPages(prefix string) ([]Page, error) {
+ w.mu.RLock()
+ defer w.mu.RUnlock()
+
+ query := "SELECT path, title, meta, modified FROM pages"
+ var args []interface{}
+ if prefix != "" {
+ query += " WHERE path LIKE ? OR path = ?"
+ args = append(args, prefix+"/%", prefix)
+ }
+ query += " ORDER BY modified DESC"
+
+ rows, err := w.db.Query(query, args...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var pages []Page
+ for rows.Next() {
+ var p Page
+ var metaStr, modified string
+ if err := rows.Scan(&p.Path, &p.Title, &metaStr, &modified); err != nil {
+ continue
+ }
+ json.Unmarshal([]byte(metaStr), &p.Frontmatter)
+ p.ModifiedAt, _ = time.Parse(time.RFC3339, modified)
+ pages = append(pages, p)
+ }
+ return pages, nil
+}
+
+// CreatePage creates a new page with the given content.
+func (w *Wiki) CreatePage(pagePath string, content string) error {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ absPath := filepath.Join(w.root, pagePath+".md")
+
+ // Ensure parent directory exists
+ if err := os.MkdirAll(filepath.Dir(absPath), 0o755); err != nil {
+ return fmt.Errorf("create directory: %w", err)
+ }
+
+ // Don't overwrite existing pages
+ if _, err := os.Stat(absPath); err == nil {
+ return fmt.Errorf("page already exists: %s", pagePath)
+ }
+
+ if err := os.WriteFile(absPath, []byte(content), 0o644); err != nil {
+ return fmt.Errorf("write page: %w", err)
+ }
+
+ return w.indexPage(pagePath)
+}
+
+// UpdatePage replaces the content of an existing page.
+func (w *Wiki) UpdatePage(pagePath string, content string) error {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ absPath := filepath.Join(w.root, pagePath+".md")
+
+ if _, err := os.Stat(absPath); os.IsNotExist(err) {
+ return fmt.Errorf("page not found: %s", pagePath)
+ }
+
+ if err := os.WriteFile(absPath, []byte(content), 0o644); err != nil {
+ return fmt.Errorf("write page: %w", err)
+ }
+
+ return w.indexPage(pagePath)
+}
+
+// DeletePage removes a page from the filesystem and index.
+func (w *Wiki) DeletePage(pagePath string) error {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ absPath := filepath.Join(w.root, pagePath+".md")
+
+ if err := os.Remove(absPath); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("delete page: %w", err)
+ }
+
+ return w.removePageIndex(pagePath)
+}
+
+// Search performs a full-text search across page titles and bodies.
+func (w *Wiki) Search(query string, limit int) ([]SearchResult, error) {
+ w.mu.RLock()
+ defer w.mu.RUnlock()
+
+ if limit <= 0 {
+ limit = 20
+ }
+
+ rows, err := w.db.Query(`
+ SELECT p.path, p.title, snippet(pages_fts, 2, '', '', '…', 32) as snip
+ FROM pages_fts
+ JOIN pages p ON p.rowid = pages_fts.rowid
+ WHERE pages_fts MATCH ?
+ ORDER BY rank
+ LIMIT ?
+ `, query, limit)
+ if err != nil {
+ return nil, fmt.Errorf("search: %w", err)
+ }
+ defer rows.Close()
+
+ var results []SearchResult
+ for rows.Next() {
+ var r SearchResult
+ if err := rows.Scan(&r.Path, &r.Title, &r.Snippet); err != nil {
+ continue
+ }
+ results = append(results, r)
+ }
+ return results, nil
+}
+
+// GetBacklinks returns paths of pages that link to the given page.
+func (w *Wiki) GetBacklinks(pagePath string) ([]string, error) {
+ w.mu.RLock()
+ defer w.mu.RUnlock()
+ return w.getBacklinks(pagePath)
+}
+
+// Context returns a WikiContext overview.
+func (w *Wiki) Context() (*WikiContext, error) {
+ w.mu.RLock()
+ defer w.mu.RUnlock()
+
+ var count int
+ w.db.QueryRow("SELECT COUNT(*) FROM pages").Scan(&count)
+
+ // Recent pages
+ rows, err := w.db.Query("SELECT path, title, modified FROM pages ORDER BY modified DESC LIMIT 20")
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var recent []Page
+ for rows.Next() {
+ var p Page
+ var modified string
+ if err := rows.Scan(&p.Path, &p.Title, &modified); err != nil {
+ continue
+ }
+ p.ModifiedAt, _ = time.Parse(time.RFC3339, modified)
+ recent = append(recent, p)
+ }
+
+ // Top-level dirs
+ dirs := w.topLevelDirs()
+
+ return &WikiContext{
+ PageCount: count,
+ RecentPages: recent,
+ TopLevelDirs: dirs,
+ }, nil
+}
+
+// --- internal helpers ---
+
+func (w *Wiki) getLinks(pagePath string) ([]string, error) {
+ rows, err := w.db.Query("SELECT target FROM links WHERE source = ?", pagePath)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var links []string
+ for rows.Next() {
+ var target string
+ if err := rows.Scan(&target); err == nil {
+ links = append(links, target)
+ }
+ }
+ return links, nil
+}
+
+func (w *Wiki) getBacklinks(pagePath string) ([]string, error) {
+ rows, err := w.db.Query("SELECT source FROM links WHERE target = ?", pagePath)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var backlinks []string
+ for rows.Next() {
+ var source string
+ if err := rows.Scan(&source); err == nil {
+ backlinks = append(backlinks, source)
+ }
+ }
+ return backlinks, nil
+}
+
+func (w *Wiki) topLevelDirs() []string {
+ entries, err := os.ReadDir(w.root)
+ if err != nil {
+ return nil
+ }
+ var dirs []string
+ for _, e := range entries {
+ if e.IsDir() && !strings.HasPrefix(e.Name(), ".") {
+ dirs = append(dirs, e.Name())
+ }
+ }
+ return dirs
+}
diff --git a/internal/wiki/parse.go b/internal/wiki/parse.go
new file mode 100644
index 0000000..7cf2b80
--- /dev/null
+++ b/internal/wiki/parse.go
@@ -0,0 +1,122 @@
+package wiki
+
+import (
+ "strings"
+
+ "github.com/yuin/goldmark"
+ meta "github.com/yuin/goldmark-meta"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/text"
+)
+
+var md = goldmark.New(
+ goldmark.WithExtensions(
+ meta.Meta,
+ ),
+)
+
+// parsedPage holds the result of parsing a markdown file's raw bytes.
+type parsedPage struct {
+ title string
+ body string
+ frontmatter map[string]interface{}
+ links []string
+}
+
+// parsePage extracts frontmatter, title, body text, and wikilinks from raw markdown.
+func parsePage(raw []byte) parsedPage {
+ ctx := parser.NewContext()
+ reader := text.NewReader(raw)
+
+ // Parse to extract frontmatter via goldmark-meta
+ doc := md.Parser().Parse(reader, parser.WithContext(ctx))
+ _ = doc // we don't render here, just parse
+
+ fm := meta.Get(ctx)
+ body, fmEnd := stripFrontmatter(raw)
+
+ title := extractTitle(fm, body, "")
+
+ links := extractWikilinks(body)
+
+ _ = fmEnd
+
+ return parsedPage{
+ title: title,
+ body: string(body),
+ frontmatter: fm,
+ links: links,
+ }
+}
+
+// stripFrontmatter removes the YAML frontmatter block from raw markdown,
+// returning the body and the byte offset where frontmatter ends.
+func stripFrontmatter(raw []byte) ([]byte, int) {
+ s := string(raw)
+ if !strings.HasPrefix(s, "---") {
+ return raw, 0
+ }
+ end := strings.Index(s[3:], "---")
+ if end < 0 {
+ return raw, 0
+ }
+ offset := 3 + end + 3
+ // Skip the trailing newline after closing ---
+ if offset < len(s) && s[offset] == '\n' {
+ offset++
+ }
+ return []byte(s[offset:]), offset
+}
+
+// extractTitle gets the title from frontmatter "title" field, or falls back
+// to the first markdown heading, or the filename.
+func extractTitle(fm map[string]interface{}, body []byte, filename string) string {
+ if fm != nil {
+ if t, ok := fm["title"]; ok {
+ if s, ok := t.(string); ok && s != "" {
+ return s
+ }
+ }
+ }
+
+ // Look for first # heading
+ for _, line := range strings.Split(string(body), "\n") {
+ trimmed := strings.TrimSpace(line)
+ if strings.HasPrefix(trimmed, "# ") {
+ return strings.TrimSpace(trimmed[2:])
+ }
+ }
+
+ return filename
+}
+
+// extractWikilinks finds all [[target]] patterns in markdown text.
+// Returns deduplicated target strings.
+func extractWikilinks(body []byte) []string {
+ s := string(body)
+ seen := make(map[string]bool)
+ var links []string
+
+ for {
+ start := strings.Index(s, "[[")
+ if start < 0 {
+ break
+ }
+ end := strings.Index(s[start:], "]]")
+ if end < 0 {
+ break
+ }
+ target := strings.TrimSpace(s[start+2 : start+end])
+ // Handle [[display|target]] syntax
+ if pipe := strings.Index(target, "|"); pipe >= 0 {
+ target = strings.TrimSpace(target[pipe+1:])
+ }
+ if target != "" && !seen[target] {
+ seen[target] = true
+ links = append(links, target)
+ }
+ s = s[start+end+2:]
+ }
+
+ return links
+}
diff --git a/internal/wiki/wiki.go b/internal/wiki/wiki.go
new file mode 100644
index 0000000..d8d8d6f
--- /dev/null
+++ b/internal/wiki/wiki.go
@@ -0,0 +1,145 @@
+// Package wiki implements a markdown-based wiki engine backed by the filesystem
+// and indexed with SQLite FTS5. Pages are plain markdown files with optional
+// YAML frontmatter. Wikilinks ([[target]]) are first-class citizens — the engine
+// extracts them during indexing and maintains a backlink graph.
+//
+// All public methods are safe for concurrent use.
+package wiki
+
+import (
+ "database/sql"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+
+ _ "github.com/mattn/go-sqlite3" // requires CGO_ENABLED=1
+)
+
+// Page represents a single wiki page.
+type Page struct {
+ // Path relative to the wiki root, without extension (e.g. "projects/mind-map")
+ Path string `json:"path"`
+ // Title extracted from frontmatter or first heading, falling back to filename
+ Title string `json:"title"`
+ // Raw markdown content (without frontmatter)
+ Body string `json:"body"`
+ // Parsed YAML frontmatter as key-value pairs
+ Frontmatter map[string]interface{} `json:"frontmatter,omitempty"`
+ // Outgoing wikilinks (target paths)
+ Links []string `json:"links,omitempty"`
+ // Incoming links from other pages
+ Backlinks []string `json:"backlinks,omitempty"`
+ // File modification time
+ ModifiedAt time.Time `json:"modified_at"`
+}
+
+// SearchResult is a page returned from a search query with a relevance snippet.
+type SearchResult struct {
+ Path string `json:"path"`
+ Title string `json:"title"`
+ Snippet string `json:"snippet"`
+}
+
+// WikiContext provides an overview of the wiki for orientation.
+type WikiContext struct {
+ PageCount int `json:"page_count"`
+ RecentPages []Page `json:"recent_pages"`
+ TopLevelDirs []string `json:"top_level_dirs"`
+}
+
+// Wiki is the core engine. Create one with Open().
+type Wiki struct {
+ root string // absolute path to wiki directory
+ db *sql.DB // SQLite database with FTS5
+ mu sync.RWMutex
+}
+
+// Open opens (or creates) a wiki rooted at the given directory.
+// It initializes the SQLite index and performs an initial scan.
+func Open(root string) (*Wiki, error) {
+ absRoot, err := filepath.Abs(root)
+ if err != nil {
+ return nil, fmt.Errorf("resolve wiki root: %w", err)
+ }
+
+ if err := os.MkdirAll(absRoot, 0o755); err != nil {
+ return nil, fmt.Errorf("create wiki dir: %w", err)
+ }
+
+ dbPath := filepath.Join(absRoot, ".mind-map.db")
+ db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
+ if err != nil {
+ return nil, fmt.Errorf("open database: %w", err)
+ }
+
+ w := &Wiki{root: absRoot, db: db}
+ if err := w.initSchema(); err != nil {
+ db.Close()
+ return nil, fmt.Errorf("init schema: %w", err)
+ }
+
+ if err := w.Reindex(); err != nil {
+ db.Close()
+ return nil, fmt.Errorf("initial index: %w", err)
+ }
+
+ return w, nil
+}
+
+// Close releases the database connection.
+func (w *Wiki) Close() error {
+ return w.db.Close()
+}
+
+// Root returns the wiki's root directory.
+func (w *Wiki) Root() string {
+ return w.root
+}
+
+func (w *Wiki) initSchema() error {
+ schema := `
+ CREATE TABLE IF NOT EXISTS pages (
+ path TEXT PRIMARY KEY,
+ title TEXT NOT NULL DEFAULT '',
+ body TEXT NOT NULL DEFAULT '',
+ meta TEXT NOT NULL DEFAULT '{}',
+ modified TEXT NOT NULL DEFAULT ''
+ );
+
+ CREATE TABLE IF NOT EXISTS links (
+ source TEXT NOT NULL,
+ target TEXT NOT NULL,
+ PRIMARY KEY (source, target)
+ );
+
+ CREATE VIRTUAL TABLE IF NOT EXISTS pages_fts USING fts5(
+ path, title, body,
+ content='pages',
+ content_rowid='rowid'
+ );
+
+ -- Triggers to keep FTS in sync
+ CREATE TRIGGER IF NOT EXISTS pages_ai AFTER INSERT ON pages BEGIN
+ INSERT INTO pages_fts(rowid, path, title, body)
+ VALUES (new.rowid, new.path, new.title, new.body);
+ END;
+
+ CREATE TRIGGER IF NOT EXISTS pages_ad AFTER DELETE ON pages BEGIN
+ INSERT INTO pages_fts(pages_fts, rowid, path, title, body)
+ VALUES ('delete', old.rowid, old.path, old.title, old.body);
+ END;
+
+ CREATE TRIGGER IF NOT EXISTS pages_au AFTER UPDATE ON pages BEGIN
+ INSERT INTO pages_fts(pages_fts, rowid, path, title, body)
+ VALUES ('delete', old.rowid, old.path, old.title, old.body);
+ INSERT INTO pages_fts(rowid, path, title, body)
+ VALUES (new.rowid, new.path, new.title, new.body);
+ END;
+
+ CREATE INDEX IF NOT EXISTS idx_links_target ON links(target);
+ `
+ _, err := w.db.Exec(schema)
+ return err
+}
diff --git a/internal/wiki/wiki_test.go b/internal/wiki/wiki_test.go
new file mode 100644
index 0000000..514a873
--- /dev/null
+++ b/internal/wiki/wiki_test.go
@@ -0,0 +1,251 @@
+package wiki
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+// testWiki creates a temporary wiki with some test pages.
+func testWiki(t *testing.T) (*Wiki, string) {
+ t.Helper()
+ dir := t.TempDir()
+
+ // Create test pages
+ writeFile(t, dir, "index.md", `---
+title: Home
+---
+# Welcome
+
+This is the home page. See [[projects/mind-map]] and [[people/alice]].
+`)
+
+ writeFile(t, dir, "projects/mind-map.md", `---
+title: mind-map
+type: project
+status: active
+---
+# mind-map
+
+A wiki engine for AI agents. Built with [[Go]].
+
+Links to [[index]] and [[people/alice]].
+`)
+
+ writeFile(t, dir, "people/alice.md", `# Alice
+
+Alice works on [[projects/mind-map]].
+`)
+
+ writeFile(t, dir, "Go.md", `# Go
+
+A programming language.
+`)
+
+ w, err := Open(dir)
+ if err != nil {
+ t.Fatalf("Open: %v", err)
+ }
+ t.Cleanup(func() { w.Close() })
+
+ return w, dir
+}
+
+func writeFile(t *testing.T, root, relPath, content string) {
+ t.Helper()
+ abs := filepath.Join(root, relPath)
+ os.MkdirAll(filepath.Dir(abs), 0o755)
+ if err := os.WriteFile(abs, []byte(content), 0o644); err != nil {
+ t.Fatalf("write %s: %v", relPath, err)
+ }
+}
+
+func TestOpenAndPageCount(t *testing.T) {
+ w, _ := testWiki(t)
+ ctx, err := w.Context()
+ if err != nil {
+ t.Fatalf("Context: %v", err)
+ }
+ if ctx.PageCount != 4 {
+ t.Errorf("PageCount = %d, want 4", ctx.PageCount)
+ }
+}
+
+func TestGetPage(t *testing.T) {
+ w, _ := testWiki(t)
+
+ p, err := w.GetPage("projects/mind-map")
+ if err != nil {
+ t.Fatalf("GetPage: %v", err)
+ }
+ if p.Title != "mind-map" {
+ t.Errorf("Title = %q, want %q", p.Title, "mind-map")
+ }
+ if p.Frontmatter["type"] != "project" {
+ t.Errorf("Frontmatter[type] = %v, want %q", p.Frontmatter["type"], "project")
+ }
+ // Should have links to index and people/alice and Go
+ if len(p.Links) < 2 {
+ t.Errorf("Links = %v, expected at least 2", p.Links)
+ }
+}
+
+func TestBacklinks(t *testing.T) {
+ w, _ := testWiki(t)
+
+ backlinks, err := w.GetBacklinks("projects/mind-map")
+ if err != nil {
+ t.Fatalf("GetBacklinks: %v", err)
+ }
+ // index and people/alice both link to projects/mind-map
+ if len(backlinks) != 2 {
+ t.Errorf("Backlinks = %v, want 2 entries", backlinks)
+ }
+}
+
+func TestSearch(t *testing.T) {
+ w, _ := testWiki(t)
+
+ results, err := w.Search("wiki engine", 10)
+ if err != nil {
+ t.Fatalf("Search: %v", err)
+ }
+ if len(results) == 0 {
+ t.Error("Search returned 0 results, expected at least 1")
+ }
+ if results[0].Path != "projects/mind-map" {
+ t.Errorf("First result path = %q, want %q", results[0].Path, "projects/mind-map")
+ }
+}
+
+func TestCreateAndGetPage(t *testing.T) {
+ w, _ := testWiki(t)
+
+ content := `---
+title: New Page
+---
+# New Page
+
+This is new. Links to [[index]].
+`
+ err := w.CreatePage("new-page", content)
+ if err != nil {
+ t.Fatalf("CreatePage: %v", err)
+ }
+
+ p, err := w.GetPage("new-page")
+ if err != nil {
+ t.Fatalf("GetPage after create: %v", err)
+ }
+ if p.Title != "New Page" {
+ t.Errorf("Title = %q, want %q", p.Title, "New Page")
+ }
+ if len(p.Links) != 1 || p.Links[0] != "index" {
+ t.Errorf("Links = %v, want [index]", p.Links)
+ }
+}
+
+func TestUpdatePage(t *testing.T) {
+ w, _ := testWiki(t)
+
+ newContent := `---
+title: Updated Home
+---
+# Updated Home
+
+Now links to [[Go]] only.
+`
+ err := w.UpdatePage("index", newContent)
+ if err != nil {
+ t.Fatalf("UpdatePage: %v", err)
+ }
+
+ p, err := w.GetPage("index")
+ if err != nil {
+ t.Fatalf("GetPage: %v", err)
+ }
+ if p.Title != "Updated Home" {
+ t.Errorf("Title = %q, want %q", p.Title, "Updated Home")
+ }
+ if len(p.Links) != 1 || p.Links[0] != "Go" {
+ t.Errorf("Links = %v, want [Go]", p.Links)
+ }
+}
+
+func TestDeletePage(t *testing.T) {
+ w, _ := testWiki(t)
+
+ err := w.DeletePage("Go")
+ if err != nil {
+ t.Fatalf("DeletePage: %v", err)
+ }
+
+ _, err = w.GetPage("Go")
+ if err == nil {
+ t.Error("GetPage after delete should fail")
+ }
+}
+
+func TestListPages(t *testing.T) {
+ w, _ := testWiki(t)
+
+ // All pages
+ all, err := w.ListPages("")
+ if err != nil {
+ t.Fatalf("ListPages: %v", err)
+ }
+ if len(all) != 4 {
+ t.Errorf("ListPages('') = %d pages, want 4", len(all))
+ }
+
+ // Filtered by prefix
+ projects, err := w.ListPages("projects")
+ if err != nil {
+ t.Fatalf("ListPages(projects): %v", err)
+ }
+ if len(projects) != 1 {
+ t.Errorf("ListPages('projects') = %d pages, want 1", len(projects))
+ }
+}
+
+func TestWikilinks(t *testing.T) {
+ tests := []struct {
+ input string
+ want []string
+ }{
+ {"See [[foo]] and [[bar]]", []string{"foo", "bar"}},
+ {"[[display|target]]", []string{"target"}},
+ {"No links here", nil},
+ {"[[dup]] and [[dup]]", []string{"dup"}},
+ {"[[ spaces ]]", []string{"spaces"}},
+ }
+
+ for _, tt := range tests {
+ got := extractWikilinks([]byte(tt.input))
+ if len(got) != len(tt.want) {
+ t.Errorf("extractWikilinks(%q) = %v, want %v", tt.input, got, tt.want)
+ continue
+ }
+ for i := range got {
+ if got[i] != tt.want[i] {
+ t.Errorf("extractWikilinks(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.want[i])
+ }
+ }
+ }
+}
+
+func TestContextTopLevelDirs(t *testing.T) {
+ w, _ := testWiki(t)
+ ctx, err := w.Context()
+ if err != nil {
+ t.Fatalf("Context: %v", err)
+ }
+ // Should have "projects" and "people"
+ found := map[string]bool{}
+ for _, d := range ctx.TopLevelDirs {
+ found[d] = true
+ }
+ if !found["projects"] || !found["people"] {
+ t.Errorf("TopLevelDirs = %v, expected projects and people", ctx.TopLevelDirs)
+ }
+}
diff --git a/testdata/index.md b/testdata/index.md
new file mode 100644
index 0000000..5986c15
--- /dev/null
+++ b/testdata/index.md
@@ -0,0 +1,8 @@
+---
+title: Welcome
+---
+# Welcome to mind-map!
+
+This is a test wiki for development.
+
+See [[projects/mind-map]] for the project page.
diff --git a/testdata/projects/mind-map.md b/testdata/projects/mind-map.md
new file mode 100644
index 0000000..d05ccac
--- /dev/null
+++ b/testdata/projects/mind-map.md
@@ -0,0 +1,10 @@
+---
+title: mind-map
+type: project
+status: active
+---
+# mind-map
+
+A wiki engine for AI agents and humans. Built with [[Go]].
+
+Links back to [[index]].
diff --git a/webui/package-lock.json b/webui/package-lock.json
new file mode 100644
index 0000000..86ccbab
--- /dev/null
+++ b/webui/package-lock.json
@@ -0,0 +1,2490 @@
+{
+ "name": "webui",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "marked": "^15.0.0",
+ "preact": "^10.25.0"
+ },
+ "devDependencies": {
+ "copy-webpack-plugin": "^13.0.0",
+ "css-loader": "^7.1.2",
+ "html-webpack-plugin": "^5.6.3",
+ "mini-css-extract-plugin": "^2.8.0",
+ "style-loader": "^4.0.0",
+ "ts-loader": "^9.5.2",
+ "typescript": "^5.7.2",
+ "webpack": "^5.99.9",
+ "webpack-cli": "^6.0.1"
+ }
+ },
+ "node_modules/@discoveryjs/json-ext": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
+ "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.17.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@types/eslint": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
+ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/eslint-scope": {
+ "version": "3.7.7",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint": "*",
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/html-minifier-terser": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
+ "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.6.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.19.0"
+ }
+ },
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+ "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+ "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+ "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+ "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/wasm-gen": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+ "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/helper-wasm-section": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-opt": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1",
+ "@webassemblyjs/wast-printer": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webpack-cli/configtest": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz",
+ "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "webpack": "^5.82.0",
+ "webpack-cli": "6.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/info": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz",
+ "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "webpack": "^5.82.0",
+ "webpack-cli": "6.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/serve": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz",
+ "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "webpack": "^5.82.0",
+ "webpack-cli": "6.x.x"
+ },
+ "peerDependenciesMeta": {
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-phases": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
+ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "peerDependencies": {
+ "acorn": "^8.14.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.23",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz",
+ "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/camel-case": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
+ "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pascal-case": "^3.1.2",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001791",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
+ "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/clean-css": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
+ "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "~0.6.0"
+ },
+ "engines": {
+ "node": ">= 10.0"
+ }
+ },
+ "node_modules/clone-deep": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+ "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/copy-webpack-plugin": {
+ "version": "13.0.1",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz",
+ "integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob-parent": "^6.0.1",
+ "normalize-path": "^3.0.0",
+ "schema-utils": "^4.2.0",
+ "serialize-javascript": "^6.0.2",
+ "tinyglobby": "^0.2.12"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-loader": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz",
+ "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.1.0",
+ "postcss": "^8.4.40",
+ "postcss-modules-extract-imports": "^3.1.0",
+ "postcss-modules-local-by-default": "^4.0.5",
+ "postcss-modules-scope": "^3.2.0",
+ "postcss-modules-values": "^4.0.0",
+ "postcss-value-parser": "^4.2.0",
+ "semver": "^7.6.3"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0",
+ "webpack": "^5.27.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/css-select": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
+ "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.0.1",
+ "domhandler": "^4.3.1",
+ "domutils": "^2.8.0",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/dom-converter": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
+ "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "utila": "~0.4"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
+ "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.2.0",
+ "entities": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
+ "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.2.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
+ "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^1.0.1",
+ "domelementtype": "^2.2.0",
+ "domhandler": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dot-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
+ "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.344",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
+ "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
+ "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/envinfo": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz",
+ "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "envinfo": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/html-minifier-terser": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
+ "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camel-case": "^4.1.2",
+ "clean-css": "^5.2.2",
+ "commander": "^8.3.0",
+ "he": "^1.2.0",
+ "param-case": "^3.0.4",
+ "relateurl": "^0.2.7",
+ "terser": "^5.10.0"
+ },
+ "bin": {
+ "html-minifier-terser": "cli.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/html-webpack-plugin": {
+ "version": "5.6.7",
+ "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz",
+ "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/html-minifier-terser": "^6.0.0",
+ "html-minifier-terser": "^6.0.2",
+ "lodash": "^4.17.21",
+ "pretty-error": "^4.0.0",
+ "tapable": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/html-webpack-plugin"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || 1.x",
+ "webpack": "^5.20.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
+ "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
+ "dev": true,
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.0.0",
+ "domutils": "^2.5.2",
+ "entities": "^2.0.0"
+ }
+ },
+ "node_modules/icss-utils": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+ "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
+ "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/interpret": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
+ "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/loader-runner": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz",
+ "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.11.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lower-case": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
+ "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/marked": {
+ "version": "15.0.12",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
+ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mini-css-extract-plugin": {
+ "version": "2.10.2",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz",
+ "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "schema-utils": "^4.0.0",
+ "tapable": "^2.2.1"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/no-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
+ "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lower-case": "^2.0.2",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.38",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/param-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
+ "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dot-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/pascal-case": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
+ "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.12",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
+ "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-modules-extract-imports": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
+ "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz",
+ "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.0.0",
+ "postcss-selector-parser": "^7.0.0",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-scope": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
+ "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-values": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+ "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "icss-utils": "^5.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
+ "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/preact": {
+ "version": "10.29.1",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
+ "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
+ "node_modules/pretty-error": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
+ "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.20",
+ "renderkid": "^3.0.0"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
+ "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve": "^1.20.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/relateurl": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+ "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/renderkid": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
+ "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-select": "^4.1.3",
+ "dom-converter": "^0.2.0",
+ "htmlparser2": "^6.1.0",
+ "lodash": "^4.17.21",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/schema-utils": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/shallow-clone": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+ "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/style-loader": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz",
+ "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.27.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.46.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz",
+ "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz",
+ "integrity": "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^4.3.0",
+ "terser": "^5.31.1"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-loader": {
+ "version": "9.5.7",
+ "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.7.tgz",
+ "integrity": "sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "enhanced-resolve": "^5.0.0",
+ "micromatch": "^4.0.0",
+ "semver": "^7.3.4",
+ "source-map": "^0.7.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "typescript": "*",
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/ts-loader/node_modules/source-map": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+ "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.19.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/utila": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
+ "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/watchpack": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
+ "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack": {
+ "version": "5.106.2",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz",
+ "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/eslint-scope": "^3.7.7",
+ "@types/estree": "^1.0.8",
+ "@types/json-schema": "^7.0.15",
+ "@webassemblyjs/ast": "^1.14.1",
+ "@webassemblyjs/wasm-edit": "^1.14.1",
+ "@webassemblyjs/wasm-parser": "^1.14.1",
+ "acorn": "^8.16.0",
+ "acorn-import-phases": "^1.0.3",
+ "browserslist": "^4.28.1",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.20.0",
+ "es-module-lexer": "^2.0.0",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.11",
+ "loader-runner": "^4.3.1",
+ "mime-db": "^1.54.0",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^4.3.3",
+ "tapable": "^2.3.0",
+ "terser-webpack-plugin": "^5.3.17",
+ "watchpack": "^2.5.1",
+ "webpack-sources": "^3.3.4"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-cli": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz",
+ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@discoveryjs/json-ext": "^0.6.1",
+ "@webpack-cli/configtest": "^3.0.1",
+ "@webpack-cli/info": "^3.0.1",
+ "@webpack-cli/serve": "^3.0.1",
+ "colorette": "^2.0.14",
+ "commander": "^12.1.0",
+ "cross-spawn": "^7.0.3",
+ "envinfo": "^7.14.0",
+ "fastest-levenshtein": "^1.0.12",
+ "import-local": "^3.0.2",
+ "interpret": "^3.1.1",
+ "rechoir": "^0.8.0",
+ "webpack-merge": "^6.0.1"
+ },
+ "bin": {
+ "webpack-cli": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.82.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack-bundle-analyzer": {
+ "optional": true
+ },
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-cli/node_modules/commander": {
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+ "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webpack-merge": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz",
+ "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-deep": "^4.0.1",
+ "flat": "^5.0.2",
+ "wildcard": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.0.tgz",
+ "integrity": "sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wildcard": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
+ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/webui/package.json b/webui/package.json
new file mode 100644
index 0000000..fc3df91
--- /dev/null
+++ b/webui/package.json
@@ -0,0 +1,21 @@
+{
+ "scripts": {
+ "build": "webpack --mode production --config webpack.config.js",
+ "dev": "webpack --mode development --config webpack.config.js --watch"
+ },
+ "dependencies": {
+ "preact": "^10.25.0",
+ "marked": "^15.0.0"
+ },
+ "devDependencies": {
+ "copy-webpack-plugin": "^13.0.0",
+ "css-loader": "^7.1.2",
+ "html-webpack-plugin": "^5.6.3",
+ "style-loader": "^4.0.0",
+ "mini-css-extract-plugin": "^2.8.0",
+ "ts-loader": "^9.5.2",
+ "typescript": "^5.7.2",
+ "webpack": "^5.99.9",
+ "webpack-cli": "^6.0.1"
+ }
+}
diff --git a/webui/src/App.tsx b/webui/src/App.tsx
new file mode 100644
index 0000000..cab5621
--- /dev/null
+++ b/webui/src/App.tsx
@@ -0,0 +1,212 @@
+import { useState, useEffect } from 'preact/hooks';
+import { mcp, Page } from './mcp';
+import { marked } from 'marked';
+
+export function App() {
+ const [pages, setPages] = useState([]);
+ const [current, setCurrent] = useState(null);
+ const [editing, setEditing] = useState(false);
+ const [editContent, setEditContent] = useState('');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [isDark, setIsDark] = useState(() => {
+ const saved = localStorage.getItem('mm-theme');
+ if (saved) return saved === 'dark';
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
+ });
+
+ useEffect(() => {
+ document.documentElement.classList.toggle('dark', isDark);
+ localStorage.setItem('mm-theme', isDark ? 'dark' : 'light');
+ }, [isDark]);
+
+ // Load page list
+ const loadPages = async () => {
+ try {
+ const list = await mcp.listPages();
+ setPages(list);
+ } catch (e) {
+ console.error('Failed to load pages:', e);
+ }
+ };
+
+ useEffect(() => { loadPages(); }, []);
+
+ // Hash routing
+ const getHashPath = (): string | null => {
+ const hash = window.location.hash.replace(/^#\/?/, '');
+ return hash || null;
+ };
+
+ useEffect(() => {
+ const onHash = () => {
+ const path = getHashPath();
+ if (path) openPage(path);
+ else setCurrent(null);
+ };
+ window.addEventListener('hashchange', onHash);
+ // Load initial page from hash
+ const initial = getHashPath();
+ if (initial) openPage(initial);
+ return () => window.removeEventListener('hashchange', onHash);
+ }, []);
+
+ const navigate = (path: string | null) => {
+ window.location.hash = path ? `/${path}` : '/';
+ };
+
+ const openPage = async (path: string) => {
+ try {
+ const page = await mcp.getPage(path);
+ setCurrent(page);
+ setEditing(false);
+ } catch (e) {
+ console.error('Failed to open page:', e);
+ }
+ };
+
+ const handleSave = async () => {
+ if (!current) return;
+ try {
+ await mcp.updatePage(current.path, editContent);
+ await openPage(current.path);
+ await loadPages();
+ } catch (e) {
+ console.error('Failed to save:', e);
+ }
+ };
+
+ const handleEdit = () => {
+ if (!current) return;
+ // Reconstruct full content with frontmatter
+ let content = '';
+ if (current.frontmatter && Object.keys(current.frontmatter).length > 0) {
+ content += '---\n';
+ for (const [k, v] of Object.entries(current.frontmatter)) {
+ content += `${k}: ${v}\n`;
+ }
+ content += '---\n';
+ }
+ content += current.body;
+ setEditContent(content);
+ setEditing(true);
+ };
+
+ const handleSearch = async () => {
+ if (!searchQuery.trim()) {
+ loadPages();
+ return;
+ }
+ try {
+ const results = await mcp.searchPages(searchQuery);
+ setPages(results.map(r => ({ path: r.path, title: r.title, body: '', modified_at: '' })));
+ } catch (e) {
+ console.error('Search failed:', e);
+ }
+ };
+
+ const renderMarkdown = (body: string): string => {
+ // Convert [[wikilinks]] to clickable links before rendering
+ const withLinks = body.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_, target, display) => {
+ const label = display || target;
+ return `[${label}](#/${target})`;
+ });
+ return marked.parse(withLinks, { async: false }) as string;
+ };
+
+ const pageCount = pages.length;
+
+ return (
+
+ {/* Sidebar */}
+
+
+ {/* Main */}
+
+ {current ? (
+ <>
+
+
+ {editing ? (
+
+
+ ) : (
+ <>
+
+ {current.backlinks && current.backlinks.length > 0 && (
+
+
Linked from
+ {current.backlinks.map(bl => (
+
navigate(bl)}>
+ {bl}
+
+ ))}
+
+ )}
+ >
+ )}
+ >
+ ) : (
+
select a page
+ )}
+
+
+ );
+}
diff --git a/webui/src/index.html b/webui/src/index.html
new file mode 100644
index 0000000..64c541f
--- /dev/null
+++ b/webui/src/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ mind-map
+
+
+
+
+
+
+
+
diff --git a/webui/src/index.tsx b/webui/src/index.tsx
new file mode 100644
index 0000000..001670b
--- /dev/null
+++ b/webui/src/index.tsx
@@ -0,0 +1,5 @@
+import { render } from 'preact';
+import { App } from './App';
+import './styles.css';
+
+render(, document.getElementById('root')!);
diff --git a/webui/src/mcp.ts b/webui/src/mcp.ts
new file mode 100644
index 0000000..06fec22
--- /dev/null
+++ b/webui/src/mcp.ts
@@ -0,0 +1,154 @@
+/** MCP client over SSE — talks to mind-map's /mcp endpoint */
+
+export interface Page {
+ path: string;
+ title: string;
+ body: string;
+ frontmatter?: Record;
+ links?: string[];
+ backlinks?: string[];
+ modified_at?: string;
+}
+
+export interface SearchResult {
+ path: string;
+ title: string;
+ snippet: string;
+}
+
+export interface WikiContext {
+ page_count: number;
+ recent_pages: Page[];
+ top_level_dirs: string[];
+}
+
+type MCPResult = { content: Array<{ type: string; text: string }> };
+
+class MCPClient {
+ private endpoint: string;
+ private sessionEndpoint: string | null = null;
+ private requestId = 0;
+ private pending = new Map void; reject: (e: any) => void }>();
+ private eventSource: EventSource | null = null;
+ private ready: Promise;
+ private resolveReady!: () => void;
+
+ constructor(endpoint: string) {
+ this.endpoint = endpoint;
+ this.ready = new Promise(r => { this.resolveReady = r; });
+ this.connect();
+ }
+
+ private connect() {
+ this.eventSource = new EventSource(this.endpoint);
+
+ this.eventSource.addEventListener('endpoint', (e: MessageEvent) => {
+ const base = new URL(this.endpoint, window.location.href);
+ this.sessionEndpoint = new URL(e.data, base).href;
+ this.initialize();
+ });
+
+ this.eventSource.addEventListener('message', (e: MessageEvent) => {
+ try {
+ const msg = JSON.parse(e.data);
+ if (msg.id !== undefined && this.pending.has(msg.id)) {
+ const p = this.pending.get(msg.id)!;
+ this.pending.delete(msg.id);
+ if (msg.error) {
+ p.reject(new Error(msg.error.message || 'MCP error'));
+ } else {
+ p.resolve(msg.result);
+ }
+ }
+ } catch { /* ignore non-JSON */ }
+ });
+
+ this.eventSource.onerror = () => {
+ console.error('SSE connection lost, reconnecting...');
+ setTimeout(() => this.connect(), 2000);
+ };
+ }
+
+ private async initialize() {
+ await this.send('initialize', {
+ protocolVersion: '2024-11-05',
+ capabilities: {},
+ clientInfo: { name: 'mind-map-webui', version: '0.1.0' },
+ });
+ await this.send('notifications/initialized', undefined, true);
+ this.resolveReady();
+ }
+
+ private send(method: string, params?: any, isNotification = false): Promise {
+ return new Promise((resolve, reject) => {
+ if (!this.sessionEndpoint) {
+ reject(new Error('Not connected'));
+ return;
+ }
+
+ const id = isNotification ? undefined : ++this.requestId;
+ const msg: any = { jsonrpc: '2.0', method };
+ if (id !== undefined) msg.id = id;
+ if (params !== undefined) msg.params = params;
+
+ if (id !== undefined) {
+ this.pending.set(id, { resolve, reject });
+ }
+
+ fetch(this.sessionEndpoint, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(msg),
+ }).then(r => {
+ if (!r.ok) reject(new Error(`HTTP ${r.status}`));
+ if (isNotification) resolve(undefined);
+ }).catch(reject);
+ });
+ }
+
+ async callTool(name: string, args: Record = {}): Promise {
+ await this.ready;
+ const result = await this.send('tools/call', { name, arguments: args }) as MCPResult;
+ const text = result.content?.find((c: any) => c.type === 'text');
+ return text?.text || '';
+ }
+
+ async getWikiContext(): Promise {
+ const text = await this.callTool('get_wiki_context');
+ return JSON.parse(text);
+ }
+
+ async getPage(path: string): Promise {
+ const text = await this.callTool('get_page', { path });
+ return JSON.parse(text);
+ }
+
+ async listPages(prefix = ''): Promise {
+ const text = await this.callTool('list_pages', { prefix });
+ return JSON.parse(text) || [];
+ }
+
+ async searchPages(query: string, limit = 20): Promise {
+ const text = await this.callTool('search_pages', { query, limit });
+ return JSON.parse(text) || [];
+ }
+
+ async createPage(path: string, content: string): Promise {
+ await this.callTool('create_page', { path, content });
+ }
+
+ async updatePage(path: string, content: string): Promise {
+ await this.callTool('update_page', { path, content });
+ }
+
+ async deletePage(path: string): Promise {
+ await this.callTool('delete_page', { path });
+ }
+
+ async getBacklinks(path: string): Promise {
+ const text = await this.callTool('get_backlinks', { path });
+ return JSON.parse(text) || [];
+ }
+}
+
+export const mcp = new MCPClient('/mcp');
diff --git a/webui/src/styles.css b/webui/src/styles.css
new file mode 100644
index 0000000..8ba6efb
--- /dev/null
+++ b/webui/src/styles.css
@@ -0,0 +1,313 @@
+/* Metro-inspired wiki design — chromeless, typography-heavy */
+
+:root {
+ --bg: #ffffff;
+ --fg: #000000;
+ --fg-muted: #666666;
+ --fg-dim: #999999;
+ --accent: #0078d4;
+ --link: #0078d4;
+ --tile-bg: #f2f2f2;
+ --code-bg: #f5f5f5;
+ --border: #e0e0e0;
+ --font: 'Inter', -apple-system, system-ui, sans-serif;
+ --font-mono: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
+}
+
+html.dark {
+ --bg: #000000;
+ --fg: #ffffff;
+ --fg-muted: #a0a0a0;
+ --fg-dim: #666666;
+ --accent: #4cc2ff;
+ --link: #4cc2ff;
+ --tile-bg: #1a1a1a;
+ --code-bg: #1a1a1a;
+ --border: #333333;
+}
+
+* { margin: 0; padding: 0; box-sizing: border-box; }
+
+html, body, #root {
+ height: 100%;
+ background: var(--bg);
+ color: var(--fg);
+ font-family: var(--font);
+ -webkit-font-smoothing: antialiased;
+ overflow-x: hidden;
+}
+
+/* --- Layout --- */
+
+.app {
+ height: 100%;
+ display: flex;
+}
+
+.sidebar {
+ width: 240px;
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ overflow-y: auto;
+}
+
+.sidebar-header {
+ padding: 24px 20px 8px;
+ font-size: 20px;
+ font-weight: 200;
+ letter-spacing: -0.5px;
+}
+
+.sidebar-search {
+ padding: 8px 16px;
+}
+
+.sidebar-search input {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid var(--border);
+ border-radius: 0;
+ background: var(--bg);
+ color: var(--fg);
+ font-family: var(--font);
+ font-size: 13px;
+ outline: none;
+}
+
+.sidebar-search input:focus {
+ border-color: var(--accent);
+}
+
+.page-list {
+ flex: 1;
+ overflow-y: auto;
+ list-style: none;
+}
+
+.page-item {
+ padding: 8px 20px;
+ font-size: 14px;
+ cursor: pointer;
+ transition: background 0.1s;
+ border-left: 3px solid transparent;
+}
+
+.page-item:hover { background: var(--tile-bg); }
+
+.page-item.active {
+ background: var(--tile-bg);
+ border-left-color: var(--accent);
+ font-weight: 500;
+}
+
+.page-item-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.page-item-path {
+ font-size: 11px;
+ color: var(--fg-dim);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* --- Main content --- */
+
+.main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.page-header {
+ padding: 32px 40px 16px;
+ border-bottom: 1px solid var(--border);
+}
+
+.page-title {
+ font-size: 36px;
+ font-weight: 200;
+ letter-spacing: -1px;
+}
+
+.page-meta {
+ font-size: 12px;
+ color: var(--fg-dim);
+ margin-top: 4px;
+ display: flex;
+ gap: 16px;
+}
+
+.page-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 12px;
+}
+
+.page-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px 40px 40px;
+}
+
+/* --- Rendered markdown --- */
+
+.markdown h1 { font-size: 28px; font-weight: 300; margin: 24px 0 12px; }
+.markdown h2 { font-size: 22px; font-weight: 400; margin: 20px 0 10px; }
+.markdown h3 { font-size: 17px; font-weight: 500; margin: 16px 0 8px; }
+.markdown p { font-size: 15px; line-height: 1.7; margin: 8px 0; }
+.markdown ul, .markdown ol { padding-left: 24px; margin: 8px 0; }
+.markdown li { font-size: 15px; line-height: 1.7; }
+.markdown a { color: var(--link); text-decoration: none; }
+.markdown a:hover { text-decoration: underline; }
+.markdown code {
+ font-family: var(--font-mono);
+ font-size: 13px;
+ background: var(--code-bg);
+ padding: 2px 6px;
+}
+.markdown pre {
+ background: var(--code-bg);
+ padding: 16px;
+ overflow-x: auto;
+ margin: 12px 0;
+ font-size: 13px;
+}
+.markdown pre code { background: none; padding: 0; }
+.markdown blockquote {
+ border-left: 3px solid var(--accent);
+ padding-left: 16px;
+ color: var(--fg-muted);
+ margin: 12px 0;
+}
+.markdown hr {
+ border: none;
+ border-top: 1px solid var(--border);
+ margin: 20px 0;
+}
+
+/* --- Backlinks --- */
+
+.backlinks {
+ border-top: 1px solid var(--border);
+ padding: 20px 40px;
+}
+
+.backlinks-title {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ color: var(--fg-dim);
+ margin-bottom: 8px;
+}
+
+.backlink-item {
+ font-size: 14px;
+ color: var(--link);
+ cursor: pointer;
+ padding: 4px 0;
+}
+
+.backlink-item:hover { text-decoration: underline; }
+
+/* --- Editor --- */
+
+.editor-container {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px 40px 40px;
+}
+
+.editor-textarea {
+ width: 100%;
+ height: 100%;
+ min-height: 400px;
+ border: none;
+ background: var(--bg);
+ color: var(--fg);
+ font-family: var(--font-mono);
+ font-size: 14px;
+ line-height: 1.6;
+ resize: none;
+ outline: none;
+}
+
+/* --- Buttons --- */
+
+.btn {
+ font-family: var(--font);
+ font-size: 13px;
+ font-weight: 500;
+ padding: 6px 16px;
+ border: 1px solid var(--border);
+ background: transparent;
+ color: var(--fg);
+ cursor: pointer;
+ transition: background 0.15s, color 0.15s;
+}
+
+.btn:hover { background: var(--fg); color: var(--bg); }
+
+.btn.primary {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: #fff;
+}
+
+.btn.primary:hover { opacity: 0.85; }
+
+/* --- Status bar --- */
+
+.status-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 6px 20px;
+ font-size: 12px;
+ color: var(--fg-dim);
+ border-top: 1px solid var(--border);
+}
+
+/* --- Theme toggle --- */
+
+.theme-toggle {
+ font-family: var(--font);
+ background: none;
+ border: none;
+ color: var(--fg-dim);
+ cursor: pointer;
+ font-size: 16px;
+ padding: 4px 8px;
+}
+
+.theme-toggle:hover { color: var(--fg); }
+
+/* --- Empty state --- */
+
+.empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ color: var(--fg-dim);
+ font-size: 16px;
+ font-weight: 300;
+}
+
+/* --- Mobile --- */
+
+@media (max-width: 767px) {
+ .sidebar { width: 100%; position: absolute; z-index: 10; height: 100%; display: none; }
+ .sidebar.open { display: flex; }
+ .page-header { padding: 24px 20px 12px; }
+ .page-title { font-size: 28px; }
+ .page-body, .editor-container { padding: 16px 20px 20px; }
+ .backlinks { padding: 16px 20px; }
+}
diff --git a/webui/tsconfig.json b/webui/tsconfig.json
new file mode 100644
index 0000000..9c051f7
--- /dev/null
+++ b/webui/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "sourceMap": true,
+ "paths": {
+ "react": ["./node_modules/preact/compat"],
+ "react-dom": ["./node_modules/preact/compat"]
+ }
+ },
+ "include": ["src/**/*"]
+}
diff --git a/webui/webpack.config.js b/webui/webpack.config.js
new file mode 100644
index 0000000..78844d6
--- /dev/null
+++ b/webui/webpack.config.js
@@ -0,0 +1,42 @@
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+
+module.exports = {
+ entry: './src/index.tsx',
+ output: {
+ filename: 'index.js',
+ path: path.resolve(__dirname, 'dist'),
+ clean: true,
+ },
+ resolve: {
+ extensions: ['.tsx', '.ts', '.js'],
+ alias: {
+ 'react': 'preact/compat',
+ 'react-dom': 'preact/compat',
+ 'react/jsx-runtime': 'preact/jsx-runtime',
+ },
+ },
+ module: {
+ rules: [
+ {
+ test: /\.tsx?$/,
+ use: 'ts-loader',
+ exclude: /node_modules/,
+ },
+ {
+ test: /\.css$/i,
+ use: [MiniCssExtractPlugin.loader, 'css-loader'],
+ },
+ ],
+ },
+ mode: 'production',
+ devtool: 'source-map',
+ plugins: [
+ new HtmlWebpackPlugin({
+ template: './src/index.html',
+ filename: 'index.html',
+ }),
+ new MiniCssExtractPlugin({ filename: 'index.css' }),
+ ],
+};