From a91491da17362d5f19e35cc105268013827512d4 Mon Sep 17 00:00:00 2001 From: aniongithub Date: Sun, 26 Apr 2026 12:02:28 -0700 Subject: [PATCH 01/20] Add wiki engine core with devcontainer - Devcontainer with Go 1.26 + Node for development - Wiki engine (internal/wiki/): pages as markdown files, SQLite FTS5 search, YAML frontmatter parsing, wikilink extraction, backlink index - Page CRUD: Create, Read, Update, Delete with concurrent-safe RWMutex - Cobra CLI scaffold with 'serve' subcommand - 10 passing tests covering all wiki operations --- .devcontainer/Dockerfile | 26 ++++ .devcontainer/devcontainer.json | 19 +++ .gitignore | 3 + cmd/mind-map/main.go | 40 +++++ go.mod | 16 ++ go.sum | 19 +++ internal/wiki/index.go | 165 ++++++++++++++++++++ internal/wiki/pages.go | 257 ++++++++++++++++++++++++++++++++ internal/wiki/parse.go | 122 +++++++++++++++ internal/wiki/wiki.go | 145 ++++++++++++++++++ internal/wiki/wiki_test.go | 251 +++++++++++++++++++++++++++++++ 11 files changed, 1063 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .gitignore create mode 100644 cmd/mind-map/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/wiki/index.go create mode 100644 internal/wiki/pages.go create mode 100644 internal/wiki/parse.go create mode 100644 internal/wiki/wiki.go create mode 100644 internal/wiki/wiki_test.go diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..288f2e6 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,26 @@ +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 \ + ca-certificates \ + git \ + git-lfs \ + 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..d7f6be5 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,19 @@ +{ + "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" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4d635e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Build artifact +/mind-map +.mind-map.db diff --git a/cmd/mind-map/main.go b/cmd/mind-map/main.go new file mode 100644 index 0000000..5c0abfc --- /dev/null +++ b/cmd/mind-map/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "os" + + "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 over HTTP/SSE and serves the web UI. Agents and browsers connect to the same endpoint.", + RunE: func(cmd *cobra.Command, args []string) error { + dir, _ := cmd.Flags().GetString("dir") + addr, _ := cmd.Flags().GetString("addr") + fmt.Printf("Starting mind-map server on %s (wiki dir: %s)\n", addr, dir) + // TODO: wire up wiki + MCP + web server + return nil + }, +} + +func init() { + serveCmd.Flags().StringP("dir", "d", ".", "Path to the wiki directory") + serveCmd.Flags().StringP("addr", "a", ":8080", "Address to listen on") + rootCmd.AddCommand(serveCmd) +} + +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..b56e737 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/aniongithub/mind-map + +go 1.26.2 + +require ( + github.com/mattn/go-sqlite3 v1.14.42 + 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/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2516542 --- /dev/null +++ b/go.sum @@ -0,0 +1,19 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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/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= +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/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) + } +} From 2df07aff147e618e5311f00f5e3f6d14299bf1a3 Mon Sep 17 00:00:00 2001 From: aniongithub Date: Sun, 26 Apr 2026 12:08:10 -0700 Subject: [PATCH 02/20] Add MCP tools layer and wire up serve command - 8 MCP tools wrapping wiki engine: search_pages, get_wiki_context, get_page, create_page, update_page, delete_page, list_pages, get_backlinks - Uses official Go MCP SDK (modelcontextprotocol/go-sdk v1.5.0) - Typed input structs with jsonschema tags for auto-generated schemas - serve --stdio mode functional for single-agent MCP use - serve --addr placeholder for HTTP/SSE mode (next step) --- cmd/mind-map/main.go | 43 ++++++++-- go.mod | 7 ++ go.sum | 20 +++++ internal/mcp/server.go | 190 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 9 deletions(-) create mode 100644 internal/mcp/server.go diff --git a/cmd/mind-map/main.go b/cmd/mind-map/main.go index 5c0abfc..9c86983 100644 --- a/cmd/mind-map/main.go +++ b/cmd/mind-map/main.go @@ -4,6 +4,9 @@ import ( "fmt" "os" + "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" ) @@ -16,22 +19,44 @@ var rootCmd = &cobra.Command{ var serveCmd = &cobra.Command{ Use: "serve", Short: "Start the mind-map server", - Long: "Starts the MCP server over HTTP/SSE and serves the web UI. Agents and browsers connect to the same endpoint.", - RunE: func(cmd *cobra.Command, args []string) error { - dir, _ := cmd.Flags().GetString("dir") - addr, _ := cmd.Flags().GetString("addr") - fmt.Printf("Starting mind-map server on %s (wiki dir: %s)\n", addr, dir) - // TODO: wire up wiki + MCP + web server - return nil - }, + 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") + serveCmd.Flags().StringP("addr", "a", ":8080", "Address to listen on (HTTP/SSE mode)") + 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{}) + } + + // TODO: HTTP/SSE transport + static web app serving + addr, _ := cmd.Flags().GetString("addr") + fmt.Fprintf(os.Stderr, "mind-map server on %s (wiki: %s)\n", addr, w.Root()) + fmt.Fprintln(os.Stderr, "HTTP/SSE mode not yet implemented — use --stdio for now") + return nil +} + func main() { if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/go.mod b/go.mod index b56e737..83f1957 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,20 @@ 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 index 2516542..22c7f7a 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,38 @@ 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= diff --git a/internal/mcp/server.go b/internal/mcp/server.go new file mode 100644 index 0000000..79b5232 --- /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:"description=Search query string"` + Limit int `json:"limit,omitempty" jsonschema:"description=Maximum results (default 20)"` +} + +type pagePathInput struct { + Path string `json:"path" jsonschema:"description=Page path without .md extension (e.g. projects/mind-map)"` +} + +type createInput struct { + Path string `json:"path" jsonschema:"description=Page path without .md extension"` + Content string `json:"content" jsonschema:"description=Markdown content (optionally with YAML frontmatter)"` +} + +type updateInput struct { + Path string `json:"path" jsonschema:"description=Page path without .md extension"` + Content string `json:"content" jsonschema:"description=New markdown content"` +} + +type listInput struct { + Prefix string `json:"prefix,omitempty" jsonschema:"description=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 +} From dd4667d98a35cfe3973154ebbd966b863469592e Mon Sep 17 00:00:00 2001 From: aniongithub Date: Sun, 26 Apr 2026 12:14:05 -0700 Subject: [PATCH 03/20] Add VS Code tasks, launch configs, and extensions - tasks.json: Build, Test, Test (wiki only), Tidy - launch.json: Serve (stdio), Serve (HTTP), Test (all) with debugger - devcontainer.json: Go extensions, test explorer, YAML/ESLint/Prettier - All configs include sqlite_fts5 build tag and CGO_ENABLED=1 --- .devcontainer/devcontainer.json | 24 +++++++++++++++++++- .vscode/launch.json | 40 +++++++++++++++++++++++++++++++++ .vscode/tasks.json | 38 +++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d7f6be5..18608ae 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,5 +15,27 @@ "version": "latest" } }, - "postCreateCommand": "go version && node --version" + "postCreateCommand": "go version && node --version", + "customizations": { + "vscode": { + "extensions": [ + "golang.Go", + "golang.go-nightly", + "hbenl.vscode-test-explorer", + "ethan-reesor.vscode-go-test-adapter", + "ms-azuretools.vscode-containers", + "redhat.vscode-yaml", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint" + ], + "settings": { + "go.buildTags": "sqlite_fts5", + "go.testTags": "sqlite_fts5", + "go.buildFlags": ["-tags=sqlite_fts5"], + "go.testEnvVars": { + "CGO_ENABLED": "1" + } + } + } + } } diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2db801a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,40 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Serve (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": "Serve (HTTP)", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/mind-map", + "args": ["serve", "--addr", ":8080", "--dir", "${workspaceFolder}/testdata"], + "buildFlags": "-tags=sqlite_fts5", + "env": { + "CGO_ENABLED": "1" + } + }, + { + "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..7ef18b3 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,38 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build", + "type": "shell", + "command": "CGO_ENABLED=1 go build -tags sqlite_fts5 -o ${workspaceFolder}/mind-map ./cmd/mind-map/", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": ["$go"] + }, + { + "label": "Test", + "type": "shell", + "command": "CGO_ENABLED=1 go test -v -tags sqlite_fts5 ./...", + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": ["$go"] + }, + { + "label": "Test (wiki only)", + "type": "shell", + "command": "CGO_ENABLED=1 go test -v -tags sqlite_fts5 ./internal/wiki/...", + "group": "test", + "problemMatcher": ["$go"] + }, + { + "label": "Tidy", + "type": "shell", + "command": "go mod tidy", + "problemMatcher": [] + } + ] +} From 1cc2829c1f80bb1b4f280746493c5d776f06fc3c Mon Sep 17 00:00:00 2001 From: aniongithub Date: Sun, 26 Apr 2026 12:37:29 -0700 Subject: [PATCH 04/20] Fix jsonschema struct tags for MCP SDK The go-sdk jsonschema tag is a plain description string, not key=value format. --- internal/mcp/server.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 79b5232..69f13f7 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -79,26 +79,26 @@ func (s *Server) registerTools() { // --- Tool input types --- type searchInput struct { - Query string `json:"query" jsonschema:"description=Search query string"` - Limit int `json:"limit,omitempty" jsonschema:"description=Maximum results (default 20)"` + 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:"description=Page path without .md extension (e.g. projects/mind-map)"` + Path string `json:"path" jsonschema:"page path without .md extension, e.g. projects/mind-map"` } type createInput struct { - Path string `json:"path" jsonschema:"description=Page path without .md extension"` - Content string `json:"content" jsonschema:"description=Markdown content (optionally with YAML frontmatter)"` + 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:"description=Page path without .md extension"` - Content string `json:"content" jsonschema:"description=New markdown content"` + 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:"description=Filter pages by path prefix"` + Prefix string `json:"prefix,omitempty" jsonschema:"filter pages by path prefix"` } // --- Tool handlers --- From b3dafaad6ef693f89e920bc9dd0055fd88ed8669 Mon Sep 17 00:00:00 2001 From: aniongithub Date: Sun, 26 Apr 2026 12:47:19 -0700 Subject: [PATCH 05/20] Implement HTTP/SSE transport for MCP server - SSE endpoint at /mcp using official go-sdk SSEHandler - Graceful shutdown on SIGINT - Add testdata/ with sample wiki pages for debug launches - Both transports (stdio and HTTP/SSE) share the same server and tools --- cmd/mind-map/main.go | 30 ++++++++++++++++++++++++++++-- testdata/index.md | 8 ++++++++ testdata/projects/mind-map.md | 10 ++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 testdata/index.md create mode 100644 testdata/projects/mind-map.md diff --git a/cmd/mind-map/main.go b/cmd/mind-map/main.go index 9c86983..170ba31 100644 --- a/cmd/mind-map/main.go +++ b/cmd/mind-map/main.go @@ -1,8 +1,11 @@ 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" @@ -50,10 +53,33 @@ func runServe(cmd *cobra.Command, args []string) error { return s.MCPServer().Run(cmd.Context(), &mcp.StdioTransport{}) } - // TODO: HTTP/SSE transport + static web app serving + // 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) + // TODO: serve static Preact app at / + + 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.Fprintln(os.Stderr, "HTTP/SSE mode not yet implemented — use --stdio for now") + 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 } diff --git a/testdata/index.md b/testdata/index.md new file mode 100644 index 0000000..8b8d756 --- /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]]. From 5cccbff47c45f39f70046ccfd3cd5b61d81baf53 Mon Sep 17 00:00:00 2001 From: aniongithub Date: Sun, 26 Apr 2026 12:53:21 -0700 Subject: [PATCH 06/20] Add full MCP tool test suite via InMemoryTransport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 9 MCP tests using the SDK's InMemoryTransport — no stdio, no HTTP, pure in-process MCP protocol: client calls tool, server handles, result verified. Tests cover all 8 tools: list_tools, get_wiki_context, get_page, search_pages, get_backlinks, list_pages, create_page, update_page, delete_page. --- internal/mcp/server_test.go | 267 ++++++++++++++++++++++++++++++++++++ testdata/.mind-map.db-shm | Bin 0 -> 32768 bytes testdata/.mind-map.db-wal | Bin 0 -> 32992 bytes 3 files changed, 267 insertions(+) create mode 100644 internal/mcp/server_test.go create mode 100644 testdata/.mind-map.db-shm create mode 100644 testdata/.mind-map.db-wal 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/testdata/.mind-map.db-shm b/testdata/.mind-map.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..2e72fa1d6671f4e864d8a6ed1b632dafbef9337d GIT binary patch literal 32768 zcmeI)y9ok86a`TKzsN+e10xd~5@SOH3s}r7#0qReY{JkQM12o+F*SA0h06@fvMi^7 zJH5;sndYnGL7b!LzB=rNtHt`XUu>_F<$ZfQZ)Vl$IQ{Yc_*9+8SL%oFODoQHv=i+{ zd(nRM^FMJF0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5*BjX-X`2!S#Ma&brqlqrx~TSA~r zfn1pr0%Z#1zM>E)Qy`Z`g+Q4Cxv47z2oNApmOyT)3jqQI2oNAZfB*pk1PBlyK!5-N Y0t5&UAV7cs0RjXF5FkK+z<&um0kT3T)&Kwi literal 0 HcmV?d00001 diff --git a/testdata/.mind-map.db-wal b/testdata/.mind-map.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..a8d92f74c4b355d397da0b12f9780f4cb6f1da28 GIT binary patch literal 32992 zcmeI*?~B|-7zgmoOg5Wjca!e+>h?T@xK#_{Hs`i?w)d`5IfMfhO9jhe*WL6sw?Dbf z^^~LNdWup{O7V^Fd|U8M@kQT=|A3yag$e~rRnQj-6%+;Wd6G>jY~L#9`5kVP$;@mr z&nK@kmzjO)4dX4Xdh4L3?bA4Yxn0}%@xUv8Elpp(DbHQ(Qjxs3ym~Zze@h9y9o_1^G^_w;($iKuPV^3I-J z>@P1LvF)X>*B*MW)*Edv>QgaYCU}i&HY>xZ9<2{63zd2!3NHkeCn`}Gbpk3#FU>`- z54dF9&&tKxY;C?ed#qZUKmFv1g=6yzbH`sE9TwU0%xLB4TzvMIf=;8~4GwwZ3|}}s zqivsVg+q`2>RuEKxwjMU&M8xRCXllFal z(fjw5ZmZ&3n~oP~{>MM;9KYJNsZ6@xYVL2eL4p7TAOHafKmY;|fB*y_009U<;J+zw zK>1=~G(noGY{f$s6qL?^-5nP=u`_f1+UJ+~eF5cus=42}U(pT;0uX=z1Rwwb2tWV= z5P$##AOL|~Auz3!U!His0rwjhFgUDqT;PR=S0DXm``4QV_h(IaN}Bt;jZ>MoSLUDdZaSCc_irhVu{kbF`_gt9NM|0dsy0jESuFS^WZR*bi_bJJCF={^ z(cC}WJ6w+h0SG_<0uX=z1Rwwb2tWV=5P-m*5HNHaNsuz5TePEyOpYoY7x?hcGr8v4 z+Cuz$1nw`I`#WurAOHafKmY;|fB*y_009U<00I!$69R@*8ohf^q*pbxk`VhTD1J4- zPR9k_xw^I8uGOt%eSsUAd)vL?{ Date: Sun, 26 Apr 2026 12:53:37 -0700 Subject: [PATCH 07/20] Fix gitignore for SQLite WAL/SHM files --- .gitignore | 4 ++++ testdata/.mind-map.db-shm | Bin 32768 -> 0 bytes testdata/.mind-map.db-wal | Bin 32992 -> 0 bytes 3 files changed, 4 insertions(+) delete mode 100644 testdata/.mind-map.db-shm delete mode 100644 testdata/.mind-map.db-wal diff --git a/.gitignore b/.gitignore index a4d635e..13c8cd1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ # Build artifact /mind-map + +# Wiki database files .mind-map.db +.mind-map.db-shm +.mind-map.db-wal diff --git a/testdata/.mind-map.db-shm b/testdata/.mind-map.db-shm deleted file mode 100644 index 2e72fa1d6671f4e864d8a6ed1b632dafbef9337d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)y9ok86a`TKzsN+e10xd~5@SOH3s}r7#0qReY{JkQM12o+F*SA0h06@fvMi^7 zJH5;sndYnGL7b!LzB=rNtHt`XUu>_F<$ZfQZ)Vl$IQ{Yc_*9+8SL%oFODoQHv=i+{ zd(nRM^FMJF0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5*BjX-X`2!S#Ma&brqlqrx~TSA~r zfn1pr0%Z#1zM>E)Qy`Z`g+Q4Cxv47z2oNApmOyT)3jqQI2oNAZfB*pk1PBlyK!5-N Y0t5&UAV7cs0RjXF5FkK+z<&um0kT3T)&Kwi diff --git a/testdata/.mind-map.db-wal b/testdata/.mind-map.db-wal deleted file mode 100644 index a8d92f74c4b355d397da0b12f9780f4cb6f1da28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32992 zcmeI*?~B|-7zgmoOg5Wjca!e+>h?T@xK#_{Hs`i?w)d`5IfMfhO9jhe*WL6sw?Dbf z^^~LNdWup{O7V^Fd|U8M@kQT=|A3yag$e~rRnQj-6%+;Wd6G>jY~L#9`5kVP$;@mr z&nK@kmzjO)4dX4Xdh4L3?bA4Yxn0}%@xUv8Elpp(DbHQ(Qjxs3ym~Zze@h9y9o_1^G^_w;($iKuPV^3I-J z>@P1LvF)X>*B*MW)*Edv>QgaYCU}i&HY>xZ9<2{63zd2!3NHkeCn`}Gbpk3#FU>`- z54dF9&&tKxY;C?ed#qZUKmFv1g=6yzbH`sE9TwU0%xLB4TzvMIf=;8~4GwwZ3|}}s zqivsVg+q`2>RuEKxwjMU&M8xRCXllFal z(fjw5ZmZ&3n~oP~{>MM;9KYJNsZ6@xYVL2eL4p7TAOHafKmY;|fB*y_009U<;J+zw zK>1=~G(noGY{f$s6qL?^-5nP=u`_f1+UJ+~eF5cus=42}U(pT;0uX=z1Rwwb2tWV= z5P$##AOL|~Auz3!U!His0rwjhFgUDqT;PR=S0DXm``4QV_h(IaN}Bt;jZ>MoSLUDdZaSCc_irhVu{kbF`_gt9NM|0dsy0jESuFS^WZR*bi_bJJCF={^ z(cC}WJ6w+h0SG_<0uX=z1Rwwb2tWV=5P-m*5HNHaNsuz5TePEyOpYoY7x?hcGr8v4 z+Cuz$1nw`I`#WurAOHafKmY;|fB*y_009U<00I!$69R@*8ohf^q*pbxk`VhTD1J4- zPR9k_xw^I8uGOt%eSsUAd)vL?{ Date: Sun, 26 Apr 2026 13:07:17 -0700 Subject: [PATCH 08/20] =?UTF-8?q?Add=20Preact=20web=20UI=20=E2=80=94=20met?= =?UTF-8?q?ro-inspired=20wiki=20client=20over=20MCP=20SSE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Preact + TypeScript + webpack, same tooling as zeropoint-agent - MCP client over SSE (mcp.ts) — talks to /mcp endpoint - Wiki UI: sidebar page list, search, markdown rendering, wikilinks, backlinks, edit mode with raw markdown textarea - Metro design: chromeless, Inter font, flat UI, dark/light toggle - Go server serves webui/dist/ at / and MCP SSE at /mcp - VS Code: compound launch (server + Chrome), build-webui task, watch-webui task, waitForServer prereq - devcontainer: port 8080 forwarded, postAttachCommand installs deps --- .devcontainer/devcontainer.json | 14 +- .gitignore | 9 +- .vscode/launch.json | 114 +- .vscode/tasks.json | 96 +- cmd/mind-map/main.go | 14 +- webui/package-lock.json | 2490 +++++++++++++++++++++++++++++++ webui/package.json | 21 + webui/src/App.tsx | 212 +++ webui/src/index.html | 15 + webui/src/index.tsx | 5 + webui/src/mcp.ts | 154 ++ webui/src/styles.css | 313 ++++ webui/tsconfig.json | 20 + webui/webpack.config.js | 42 + 14 files changed, 3439 insertions(+), 80 deletions(-) create mode 100644 webui/package-lock.json create mode 100644 webui/package.json create mode 100644 webui/src/App.tsx create mode 100644 webui/src/index.html create mode 100644 webui/src/index.tsx create mode 100644 webui/src/mcp.ts create mode 100644 webui/src/styles.css create mode 100644 webui/tsconfig.json create mode 100644 webui/webpack.config.js diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 18608ae..6ba3d49 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,16 +16,15 @@ } }, "postCreateCommand": "go version && node --version", + "postAttachCommand": "npm install --prefix webui", "customizations": { "vscode": { "extensions": [ "golang.Go", - "golang.go-nightly", "hbenl.vscode-test-explorer", "ethan-reesor.vscode-go-test-adapter", - "ms-azuretools.vscode-containers", - "redhat.vscode-yaml", - "esbenp.prettier-vscode", + "idered.npm", + "qwtel.sqlite-viewer", "dbaeumer.vscode-eslint" ], "settings": { @@ -37,5 +36,12 @@ } } } + }, + "forwardPorts": [8080], + "portsAttributes": { + "8080": { + "label": "mind-map Server", + "onAutoForward": "notify" + } } } diff --git a/.gitignore b/.gitignore index 13c8cd1..eb69fe2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,14 @@ -# Build artifact +# 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/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 2db801a..cb4b667 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,40 +1,78 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "Serve (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": "Serve (HTTP)", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${workspaceFolder}/cmd/mind-map", - "args": ["serve", "--addr", ":8080", "--dir", "${workspaceFolder}/testdata"], - "buildFlags": "-tags=sqlite_fts5", - "env": { - "CGO_ENABLED": "1" - } - }, - { - "name": "Test (all)", - "type": "go", - "request": "launch", - "mode": "test", - "program": "${workspaceFolder}/internal/wiki", - "buildFlags": "-tags=sqlite_fts5", - "env": { - "CGO_ENABLED": "1" - } - } - ] + "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"], + "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"], + "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 index 7ef18b3..52df030 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,38 +1,62 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "Build", - "type": "shell", - "command": "CGO_ENABLED=1 go build -tags sqlite_fts5 -o ${workspaceFolder}/mind-map ./cmd/mind-map/", - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": ["$go"] - }, - { - "label": "Test", - "type": "shell", - "command": "CGO_ENABLED=1 go test -v -tags sqlite_fts5 ./...", - "group": { - "kind": "test", - "isDefault": true - }, - "problemMatcher": ["$go"] - }, - { - "label": "Test (wiki only)", - "type": "shell", - "command": "CGO_ENABLED=1 go test -v -tags sqlite_fts5 ./internal/wiki/...", - "group": "test", - "problemMatcher": ["$go"] - }, - { - "label": "Tidy", - "type": "shell", - "command": "go mod tidy", - "problemMatcher": [] - } - ] + "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/cmd/mind-map/main.go b/cmd/mind-map/main.go index 170ba31..bf5a038 100644 --- a/cmd/mind-map/main.go +++ b/cmd/mind-map/main.go @@ -62,7 +62,19 @@ func runServe(cmd *cobra.Command, args []string) error { mux := http.NewServeMux() mux.Handle("/mcp", sseHandler) - // TODO: serve static Preact app at / + + // Serve the webui from webui/dist/ (during development) + webDir := "webui/dist" + 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() 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 ? ( +
+