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 + +[![CI](https://github.com/aniongithub/mind-map/actions/workflows/ci.yml/badge.svg)](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 ? ( +
+