From 0de96bb183b7b36cafb76f874ac8a7cf9cf2aa3a Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Mon, 3 Aug 2026 19:45:40 -0400 Subject: [PATCH 01/83] =?UTF-8?q?feat(webapp):=20Phase=200=20=E2=80=94=20s?= =?UTF-8?q?caffold=20web=20app=20+=20lab=20status=20--json=20verb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add webapp/ (FastAPI backend skeleton + Vite/React frontend skeleton + ./dreadgoad-web launcher) and a --json output mode for 'dreadgoad lab status' that the ingestion hook will consume. Includes Go unit tests for the JSON marshaller. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 4 ++ cli/cmd/lab.go | 40 +++++++++++++++++ cli/cmd/lab_status_json_test.go | 71 +++++++++++++++++++++++++++++++ dreadgoad-web | 43 +++++++++++++++++++ webapp/backend/__init__.py | 8 ++++ webapp/backend/paths.py | 56 ++++++++++++++++++++++++ webapp/backend/requirements.txt | 8 ++++ webapp/backend/server.py | 45 ++++++++++++++++++++ webapp/frontend/index.html | 12 ++++++ webapp/frontend/package.json | 25 +++++++++++ webapp/frontend/src/App.tsx | 30 +++++++++++++ webapp/frontend/src/index.css | 47 ++++++++++++++++++++ webapp/frontend/src/main.tsx | 10 +++++ webapp/frontend/src/vite-env.d.ts | 1 + webapp/frontend/tsconfig.json | 20 +++++++++ webapp/frontend/vite.config.ts | 14 ++++++ 16 files changed, 434 insertions(+) create mode 100644 cli/cmd/lab_status_json_test.go create mode 100755 dreadgoad-web create mode 100644 webapp/backend/__init__.py create mode 100644 webapp/backend/paths.py create mode 100644 webapp/backend/requirements.txt create mode 100644 webapp/backend/server.py create mode 100644 webapp/frontend/index.html create mode 100644 webapp/frontend/package.json create mode 100644 webapp/frontend/src/App.tsx create mode 100644 webapp/frontend/src/index.css create mode 100644 webapp/frontend/src/main.tsx create mode 100644 webapp/frontend/src/vite-env.d.ts create mode 100644 webapp/frontend/tsconfig.json create mode 100644 webapp/frontend/vite.config.ts diff --git a/.gitignore b/.gitignore index 280b8b02..2f0d29cc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,10 @@ __pycache__/ dreadgoad .dreadgoad/ coverage.out + +# Web app build artifacts +webapp/frontend/node_modules/ +webapp/frontend/dist/ ansible/roles/adcs_templates/files/ADCSTemplate.zip ansible/roles/vulns_adcs_templates/files/ADCSTemplate.zip diff --git a/cli/cmd/lab.go b/cli/cmd/lab.go index 36e85fa7..906b4aa3 100644 --- a/cli/cmd/lab.go +++ b/cli/cmd/lab.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "encoding/json" "fmt" "strings" @@ -21,6 +22,10 @@ var labStatusCmd = &cobra.Command{ RunE: runLabStatus, } +// labStatusJSON toggles machine-readable JSON output for `lab status`. +// The web app's ingestion hook consumes this to refresh range state. +var labStatusJSON bool + var labStartCmd = &cobra.Command{ Use: "start", Short: "Start stopped lab instances", @@ -64,6 +69,7 @@ var labDestroyVMCmd = &cobra.Command{ func init() { rootCmd.AddCommand(labCmd) labCmd.AddCommand(labStatusCmd) + labStatusCmd.Flags().BoolVar(&labStatusJSON, "json", false, "Output machine-readable JSON (per-instance array)") labCmd.AddCommand(labStartCmd) labCmd.AddCommand(labStopCmd) labCmd.AddCommand(labStartVMCmd) @@ -96,6 +102,15 @@ func runLabStatus(cmd *cobra.Command, args []string) error { return err } + if labStatusJSON { + b, err := instancesToStatusJSON(instances) + if err != nil { + return fmt.Errorf("marshal status json: %w", err) + } + fmt.Println(string(b)) + return nil + } + if len(instances) == 0 { fmt.Printf("No GOAD instances found for env=%s\n", cfg.Env) return nil @@ -112,6 +127,31 @@ func runLabStatus(cmd *cobra.Command, args []string) error { return nil } +// statusJSONInstance is the machine-readable shape emitted by `lab status --json`. +// The web app's ingestion hook correlates `name` to config hostnames and maps +// state/private_ip/id onto range hosts (see webapp design §6.4). +type statusJSONInstance struct { + Name string `json:"name"` + ID string `json:"id"` + State string `json:"state"` + PrivateIP string `json:"private_ip"` +} + +// instancesToStatusJSON renders discovered instances as a JSON array. +// Always returns a JSON array (never null) so an empty range yields "[]". +func instancesToStatusJSON(instances []provider.Instance) ([]byte, error) { + out := make([]statusJSONInstance, 0, len(instances)) + for _, inst := range instances { + out = append(out, statusJSONInstance{ + Name: inst.Name, + ID: inst.ID, + State: inst.State, + PrivateIP: inst.PrivateIP, + }) + } + return json.MarshalIndent(out, "", " ") +} + func runLabAction(action string) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { ctx := context.Background() diff --git a/cli/cmd/lab_status_json_test.go b/cli/cmd/lab_status_json_test.go new file mode 100644 index 00000000..f529654a --- /dev/null +++ b/cli/cmd/lab_status_json_test.go @@ -0,0 +1,71 @@ +package cmd + +import ( + "encoding/json" + "testing" + + "github.com/dreadnode/dreadgoad/internal/provider" +) + +func TestInstancesToStatusJSON(t *testing.T) { + tests := []struct { + name string + instances []provider.Instance + wantLen int + }{ + { + name: "empty yields JSON array not null", + instances: nil, + wantLen: 0, + }, + { + name: "running and stopped instances", + instances: []provider.Instance{ + {ID: "i-0abc", Name: "goad-dreadgoad-kingslanding", State: "running", PrivateIP: "10.0.4.124"}, + {ID: "i-0def", Name: "goad-dreadgoad-winterfell", State: "stopped", PrivateIP: "10.0.4.76"}, + }, + wantLen: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := instancesToStatusJSON(tt.instances) + if err != nil { + t.Fatalf("instancesToStatusJSON returned error: %v", err) + } + + // Must always be a JSON array (never the literal "null"), so an + // empty range decodes to "[]" for the ingestion hook. + var decoded []statusJSONInstance + if err := json.Unmarshal(b, &decoded); err != nil { + t.Fatalf("output is not valid JSON array: %v (raw: %s)", err, b) + } + if string(b) == "null" { + t.Fatalf("empty input must render as [] not null") + } + if len(decoded) != tt.wantLen { + t.Fatalf("want %d instances, got %d", tt.wantLen, len(decoded)) + } + }) + } +} + +func TestInstancesToStatusJSONFieldMapping(t *testing.T) { + in := []provider.Instance{ + {ID: "i-0abc", Name: "goad-dreadgoad-kingslanding", State: "running", PrivateIP: "10.0.4.124"}, + } + b, err := instancesToStatusJSON(in) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + var decoded []statusJSONInstance + if err := json.Unmarshal(b, &decoded); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + got := decoded[0] + if got.ID != "i-0abc" || got.Name != "goad-dreadgoad-kingslanding" || + got.State != "running" || got.PrivateIP != "10.0.4.124" { + t.Fatalf("field mapping wrong: %+v", got) + } +} diff --git a/dreadgoad-web b/dreadgoad-web new file mode 100755 index 00000000..42915b24 --- /dev/null +++ b/dreadgoad-web @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Launch the DreadGOAD web app. +# +# ./dreadgoad-web # build frontend, serve on :8420 +# ./dreadgoad-web --dev # run vite dev server + backend (hot reload) +# +# Requires: python3, node/npm, and the compiled `dreadgoad` Go binary on PATH +# (or at cli/dreadgoad) — the backend shells out to it. See design §10. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")" && pwd)" +BACKEND="$ROOT/webapp/backend" +FRONTEND="$ROOT/webapp/frontend" +VENV="$ROOT/.venv" +PORT="${DREADGOAD_WEBAPP_PORT:-8420}" + +# --- Python venv + backend deps --- +if [ ! -d "$VENV" ]; then + echo "[dreadgoad-web] creating venv…" + python3 -m venv "$VENV" +fi +"$VENV/bin/pip" install -q -r "$BACKEND/requirements.txt" + +# --- dreadgoad CLI binary check (hard runtime dependency) --- +if ! command -v dreadgoad >/dev/null 2>&1 && [ ! -x "$ROOT/cli/dreadgoad" ]; then + echo "[dreadgoad-web] WARNING: 'dreadgoad' binary not found on PATH or at cli/dreadgoad." + echo " build it with: (cd cli && go build -o dreadgoad .)" +fi + +if [ "${1:-}" = "--dev" ]; then + echo "[dreadgoad-web] dev mode: backend :$PORT + vite dev server" + ( cd "$FRONTEND" && npm install && npm run dev ) & + exec "$VENV/bin/uvicorn" webapp.backend.server:app --reload --port "$PORT" +fi + +# --- Production-ish: build frontend, serve static from backend --- +echo "[dreadgoad-web] building frontend…" +( cd "$FRONTEND" && npm install && npm run build ) + +echo "[dreadgoad-web] serving on http://localhost:$PORT" +# mount_frontend() is invoked by an app-factory shim so the static dir is wired. +DREADGOAD_WEBAPP_FRONTEND_DIST="$FRONTEND/dist" \ + exec "$VENV/bin/uvicorn" webapp.backend.server:app --port "$PORT" diff --git a/webapp/backend/__init__.py b/webapp/backend/__init__.py new file mode 100644 index 00000000..5fe53e56 --- /dev/null +++ b/webapp/backend/__init__.py @@ -0,0 +1,8 @@ +"""DreadGOAD web app backend (FastAPI). + +Agentically build, manage, reset, and validate DreadGOAD ranges. Ported from +the ALFRED app skeleton; the PDF pane is replaced by a range network view and +the LaTeX toolset by the dreadgoad CLI toolset. +""" + +__version__ = "0.1.0" diff --git a/webapp/backend/paths.py b/webapp/backend/paths.py new file mode 100644 index 00000000..dbbfe353 --- /dev/null +++ b/webapp/backend/paths.py @@ -0,0 +1,56 @@ +"""Filesystem locations for the web app (see design §10.2). + +State lives under the gitignored ``.dreadgoad/webapp/`` runtime root: + - ``state.db`` the SQLite DB + - ``sessions/-/`` per-session working dir (agent fs sandbox) +""" + +from __future__ import annotations + +import os +from pathlib import Path + + +def repo_root() -> Path: + """Locate the DreadGOAD repo root (contains ``dreadgoad.yaml`` / ``ad/``). + + Walks up from this file; falls back to the cwd. The CLI is invoked with + ``cwd = repo_root`` so it can read ``ad/``, ``infra/``, ``dreadgoad.yaml``. + """ + here = Path(__file__).resolve() + for parent in [here, *here.parents]: + if (parent / "dreadgoad.yaml").is_file() or (parent / "ad").is_dir(): + if (parent / "ad").is_dir(): + return parent + return Path.cwd() + + +def state_root() -> Path: + """``.dreadgoad/webapp/`` under the repo root, created if missing.""" + root = repo_root() / ".dreadgoad" / "webapp" + root.mkdir(parents=True, exist_ok=True) + return root + + +def db_path() -> Path: + """Absolute path to the SQLite state DB.""" + return state_root() / "state.db" + + +def sessions_root() -> Path: + """``.dreadgoad/webapp/sessions/`` — per-session working dirs live here.""" + root = state_root() / "sessions" + root.mkdir(parents=True, exist_ok=True) + return root + + +def session_dir(dirname: str) -> Path: + """Working dir for a session (``-``), created if missing.""" + d = sessions_root() / dirname + d.mkdir(parents=True, exist_ok=True) + return d + + +# Allow overriding the DB path in tests via env var. +def resolve_db_path() -> str: + return os.environ.get("DREADGOAD_WEBAPP_DB", str(db_path())) diff --git a/webapp/backend/requirements.txt b/webapp/backend/requirements.txt new file mode 100644 index 00000000..900c6944 --- /dev/null +++ b/webapp/backend/requirements.txt @@ -0,0 +1,8 @@ +# DreadGOAD web app backend dependencies. +# Mirrors ALFRED's backend deps; the agent stack (dreadnode/rigging) is public. +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 +websockets>=12.0 +pyyaml>=6.0 +dreadnode>=1.17.0 +rigging>=3.0.0 diff --git a/webapp/backend/server.py b/webapp/backend/server.py new file mode 100644 index 00000000..d3159f48 --- /dev/null +++ b/webapp/backend/server.py @@ -0,0 +1,45 @@ +"""FastAPI backend entry point. + +Phase 0: minimal shell — health + config endpoints and static frontend mount. +Later phases add: SQLite persistence (§6), session lifecycle REST (§7), +multiplexed chat + range-state WebSockets, the agent, and the ingestion hook. +""" + +from __future__ import annotations + +import os +import typing as t + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles + +from . import __version__ as VERSION + +# Default model + provider (design decision: Sonnet 5 via OpenRouter). +_DEFAULT_MODEL = os.environ.get("DREADGOAD_WEBAPP_MODEL", "openrouter/anthropic/claude-sonnet-5") + +app = FastAPI(title="DreadGOAD Web App") + + +@app.get("/api/health") +async def health() -> dict[str, t.Any]: + """Liveness probe used by the launcher and Phase-0 manual test.""" + return {"status": "ok", "version": VERSION} + + +@app.get("/api/config") +async def get_config() -> dict[str, t.Any]: + """Static app config for the frontend shell.""" + return {"version": VERSION, "default_model": _DEFAULT_MODEL} + + +def mount_frontend(frontend_dist: str) -> None: + """Mount the built Vite frontend at ``/`` if the dist dir exists.""" + if os.path.isdir(frontend_dist): + app.mount("/", StaticFiles(directory=frontend_dist, html=True), name="frontend") + + +# The launcher sets this to the built dist dir when serving in production mode. +_frontend_dist = os.environ.get("DREADGOAD_WEBAPP_FRONTEND_DIST") +if _frontend_dist: + mount_frontend(_frontend_dist) diff --git a/webapp/frontend/index.html b/webapp/frontend/index.html new file mode 100644 index 00000000..660fe6b0 --- /dev/null +++ b/webapp/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + DreadGOAD + + +
+ + + diff --git a/webapp/frontend/package.json b/webapp/frontend/package.json new file mode 100644 index 00000000..0a98c5e7 --- /dev/null +++ b/webapp/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "dreadgoad-webapp", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@xyflow/react": "^12.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^6.0.0" + } +} diff --git a/webapp/frontend/src/App.tsx b/webapp/frontend/src/App.tsx new file mode 100644 index 00000000..05214f9d --- /dev/null +++ b/webapp/frontend/src/App.tsx @@ -0,0 +1,30 @@ +import { useEffect, useState } from 'react' + +// Phase 0 placeholder shell. Phase 5 replaces this with the two-pane +// layout (TerminalChat + RangeView) and the session tab bar. +export default function App() { + const [version, setVersion] = useState('') + const [ok, setOk] = useState(null) + + useEffect(() => { + fetch('/api/health') + .then(r => r.json()) + .then(d => { setOk(d.status === 'ok'); setVersion(d.version || '') }) + .catch(() => setOk(false)) + }, []) + + return ( +
+
+ DreadGOAD +
+
+ {ok === null ? 'connecting…' : ok ? `backend online · v${version}` : 'backend offline'} +
+
+ ) +} diff --git a/webapp/frontend/src/index.css b/webapp/frontend/src/index.css new file mode 100644 index 00000000..4e7ed05d --- /dev/null +++ b/webapp/frontend/src/index.css @@ -0,0 +1,47 @@ +:root { + /* DreadGOAD brand (ported from ALFRED, retinted) */ + --dg-brand: #ef562f; + --dg-interactive: #f5c842; + + /* Dreadnode palette */ + --dn-black: #0a0a0a; + --dn-bg: #111111; + --dn-surface: #1a1a1a; + --dn-surface-alt: #151515; + --dn-border: #1f1f1f; + --dn-border-lt: #2a2a2a; + --dn-accent: #ef562f; + --dn-accent-dim: rgba(239, 86, 47, 0.15); + --dn-text: #e0e0e0; + --dn-text-bright: #ffffff; + --dn-text-muted: #666666; + --dn-text-dim: #444444; + --dn-success: #3ecf6a; + --dn-warning: #f0a830; + --dn-error: #ef4444; + + --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +html, body, #root { + height: 100%; + width: 100%; + overflow: hidden; + background: var(--dn-black); + color: var(--dn-text); + font-family: var(--font-mono); + font-size: 14px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +::selection { background: var(--dn-accent-dim); color: var(--dn-text-bright); } + +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-track { background: var(--dn-black); } +::-webkit-scrollbar-thumb { background: var(--dn-border-lt); border-radius: 4px; } +::-webkit-scrollbar-thumb:hover { background: var(--dn-text-dim); } + +@keyframes blink { 50% { opacity: 0; } } diff --git a/webapp/frontend/src/main.tsx b/webapp/frontend/src/main.tsx new file mode 100644 index 00000000..fab12190 --- /dev/null +++ b/webapp/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import './index.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/webapp/frontend/src/vite-env.d.ts b/webapp/frontend/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/webapp/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/webapp/frontend/tsconfig.json b/webapp/frontend/tsconfig.json new file mode 100644 index 00000000..109f0ac2 --- /dev/null +++ b/webapp/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/webapp/frontend/vite.config.ts b/webapp/frontend/vite.config.ts new file mode 100644 index 00000000..3af368cd --- /dev/null +++ b/webapp/frontend/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// Dev server proxies API + WebSocket traffic to the FastAPI backend (port 8420). +export default defineConfig({ + plugins: [react()], + build: { outDir: 'dist' }, + server: { + proxy: { + '/api': 'http://localhost:8420', + '/ws': { target: 'ws://localhost:8420', ws: true }, + }, + }, +}) From 304d921d9bd8f52cadb809569de62d8a8e4bd2aa Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Mon, 3 Aug 2026 19:48:11 -0400 Subject: [PATCH 02/83] =?UTF-8?q?feat(webapp):=20Phase=201=20=E2=80=94=20S?= =?UTF-8?q?QLite=20persistence=20backbone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add db.py: async SQLite layer (sessions/ranges/events/meta, WAL) with a single-worker executor serializing all writes for concurrency safety. Event log assigns monotonic per-session seq. Standalone-runnable tests cover CRUD, seq/replay/kind-filter, and a 100-write concurrency check (no lost/duplicate seqs). Co-Authored-By: Claude Opus 4.8 --- webapp/backend/db.py | 235 ++++++++++++++++++++++++++++++++ webapp/backend/tests/test_db.py | 114 ++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 webapp/backend/db.py create mode 100644 webapp/backend/tests/test_db.py diff --git a/webapp/backend/db.py b/webapp/backend/db.py new file mode 100644 index 00000000..08354a6f --- /dev/null +++ b/webapp/backend/db.py @@ -0,0 +1,235 @@ +"""SQLite persistence layer (design §6). + +Document model over SQLite: each collection is a table whose payload is a JSON +column; only queried fields (event `session_id`/`seq`/`kind`) are real columns. + +Concurrency & safety: all DB work runs on a **single-worker thread executor**, +so operations serialize naturally (no lost updates) and the connection is only +ever touched from its own thread. WAL mode gives crash-safe commits and +concurrent readers. This is the "one serialized writer" from §6.1 without an +extra dependency (stdlib ``sqlite3`` only). +""" + +from __future__ import annotations + +import asyncio +import json +import sqlite3 +import typing as t +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timezone + +_SCHEMA = """ +CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + data TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS ranges ( + session_id TEXT PRIMARY KEY, + data TEXT NOT NULL +); +CREATE TABLE IF NOT EXISTS events ( + session_id TEXT NOT NULL, + seq INTEGER NOT NULL, + kind TEXT NOT NULL, + ts TEXT NOT NULL, + payload TEXT NOT NULL, + PRIMARY KEY (session_id, seq) +); +CREATE INDEX IF NOT EXISTS idx_events_kind ON events (session_id, kind); +CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); +""" + + +def _utcnow() -> str: + return datetime.now(timezone.utc).isoformat() + + +class Database: + """Async wrapper over a single-threaded SQLite connection.""" + + def __init__(self, path: str) -> None: + self._path = path + self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="dg-db") + self._conn: sqlite3.Connection | None = None + + # --- lifecycle --------------------------------------------------------- + + async def connect(self) -> "Database": + await self._run(self._connect) + return self + + def _connect(self) -> None: + conn = sqlite3.connect(self._path) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA synchronous=NORMAL") + conn.execute("PRAGMA foreign_keys=ON") + conn.executescript(_SCHEMA) + conn.commit() + self._conn = conn + + async def close(self) -> None: + await self._run(self._close) + self._executor.shutdown(wait=True) + + def _close(self) -> None: + if self._conn is not None: + self._conn.close() + self._conn = None + + async def _run(self, fn: t.Callable[..., t.Any], *args: t.Any) -> t.Any: + loop = asyncio.get_running_loop() + return await loop.run_in_executor(self._executor, fn, *args) + + @property + def _c(self) -> sqlite3.Connection: + if self._conn is None: + raise RuntimeError("Database not connected; call connect() first") + return self._conn + + # --- sessions ---------------------------------------------------------- + + async def upsert_session(self, session: dict[str, t.Any]) -> None: + await self._run(self._upsert_session, session) + + def _upsert_session(self, session: dict[str, t.Any]) -> None: + sid = session["id"] + self._c.execute( + "INSERT INTO sessions (id, data) VALUES (?, ?) " + "ON CONFLICT(id) DO UPDATE SET data=excluded.data", + (sid, json.dumps(session)), + ) + self._c.commit() + + async def get_session(self, session_id: str) -> dict[str, t.Any] | None: + return await self._run(self._get_session, session_id) + + def _get_session(self, session_id: str) -> dict[str, t.Any] | None: + row = self._c.execute( + "SELECT data FROM sessions WHERE id=?", (session_id,) + ).fetchone() + return json.loads(row["data"]) if row else None + + async def list_sessions(self) -> list[dict[str, t.Any]]: + return await self._run(self._list_sessions) + + def _list_sessions(self) -> list[dict[str, t.Any]]: + rows = self._c.execute("SELECT data FROM sessions").fetchall() + return [json.loads(r["data"]) for r in rows] + + async def delete_session(self, session_id: str) -> None: + await self._run(self._delete_session, session_id) + + def _delete_session(self, session_id: str) -> None: + self._c.execute("DELETE FROM sessions WHERE id=?", (session_id,)) + self._c.execute("DELETE FROM ranges WHERE session_id=?", (session_id,)) + self._c.execute("DELETE FROM events WHERE session_id=?", (session_id,)) + self._c.commit() + + # --- ranges ------------------------------------------------------------ + + async def upsert_range(self, session_id: str, rng: dict[str, t.Any]) -> None: + await self._run(self._upsert_range, session_id, rng) + + def _upsert_range(self, session_id: str, rng: dict[str, t.Any]) -> None: + self._c.execute( + "INSERT INTO ranges (session_id, data) VALUES (?, ?) " + "ON CONFLICT(session_id) DO UPDATE SET data=excluded.data", + (session_id, json.dumps(rng)), + ) + self._c.commit() + + async def get_range(self, session_id: str) -> dict[str, t.Any] | None: + return await self._run(self._get_range, session_id) + + def _get_range(self, session_id: str) -> dict[str, t.Any] | None: + row = self._c.execute( + "SELECT data FROM ranges WHERE session_id=?", (session_id,) + ).fetchone() + return json.loads(row["data"]) if row else None + + # --- events ------------------------------------------------------------ + + async def append_event( + self, session_id: str, kind: str, payload: dict[str, t.Any] + ) -> int: + """Append an event, assigning a monotonic per-session ``seq``. + + Returns the assigned seq. Runs on the single DB thread, so the + read-then-insert is atomic with respect to other DB operations. + """ + return await self._run(self._append_event, session_id, kind, payload) + + def _append_event( + self, session_id: str, kind: str, payload: dict[str, t.Any] + ) -> int: + row = self._c.execute( + "SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM events WHERE session_id=?", + (session_id,), + ).fetchone() + seq = int(row["next"]) + self._c.execute( + "INSERT INTO events (session_id, seq, kind, ts, payload) VALUES (?, ?, ?, ?, ?)", + (session_id, seq, kind, _utcnow(), json.dumps(payload)), + ) + self._c.commit() + return seq + + async def get_events( + self, session_id: str, kinds: t.Sequence[str] | None = None + ) -> list[dict[str, t.Any]]: + """Return events for a session ordered by ``seq``. + + If ``kinds`` is given, filter to those event kinds (e.g. chat-kinds + for replay). Each returned dict is ``{seq, kind, ts, payload}``. + """ + return await self._run(self._get_events, session_id, kinds) + + def _get_events( + self, session_id: str, kinds: t.Sequence[str] | None + ) -> list[dict[str, t.Any]]: + if kinds: + placeholders = ",".join("?" for _ in kinds) + sql = ( + f"SELECT seq, kind, ts, payload FROM events " + f"WHERE session_id=? AND kind IN ({placeholders}) ORDER BY seq" + ) + rows = self._c.execute(sql, (session_id, *kinds)).fetchall() + else: + rows = self._c.execute( + "SELECT seq, kind, ts, payload FROM events WHERE session_id=? ORDER BY seq", + (session_id,), + ).fetchall() + return [ + { + "seq": r["seq"], + "kind": r["kind"], + "ts": r["ts"], + "payload": json.loads(r["payload"]), + } + for r in rows + ] + + # --- meta -------------------------------------------------------------- + + async def set_meta(self, key: str, value: t.Any) -> None: + await self._run(self._set_meta, key, value) + + def _set_meta(self, key: str, value: t.Any) -> None: + self._c.execute( + "INSERT INTO meta (key, value) VALUES (?, ?) " + "ON CONFLICT(key) DO UPDATE SET value=excluded.value", + (key, json.dumps(value)), + ) + self._c.commit() + + async def get_meta(self, key: str) -> t.Any | None: + return await self._run(self._get_meta, key) + + def _get_meta(self, key: str) -> t.Any | None: + row = self._c.execute("SELECT value FROM meta WHERE key=?", (key,)).fetchone() + return json.loads(row["value"]) if row else None diff --git a/webapp/backend/tests/test_db.py b/webapp/backend/tests/test_db.py new file mode 100644 index 00000000..f7190fa8 --- /dev/null +++ b/webapp/backend/tests/test_db.py @@ -0,0 +1,114 @@ +"""Tests for the SQLite layer (Phase 1: T1.1 + T1.2). + +Runnable two ways: + - standalone: python webapp/backend/tests/test_db.py (no pytest needed) + - pytest: (with pytest-asyncio installed) +""" + +from __future__ import annotations + +import asyncio +import pathlib +import sys +import tempfile + +# Make `webapp.backend.db` importable when run as a standalone script. +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[3])) + +from webapp.backend.db import Database # noqa: E402 + + +async def _fresh_db() -> tuple[Database, str]: + tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False) + tmp.close() + db = await Database(tmp.name).connect() + return db, tmp.name + + +async def test_session_and_range_crud() -> None: + db, _ = await _fresh_db() + try: + s = {"id": "s-1", "label": "test", "status": "new"} + await db.upsert_session(s) + assert await db.get_session("s-1") == s, "session round-trip failed" + + # update + s["status"] = "running" + await db.upsert_session(s) + assert (await db.get_session("s-1"))["status"] == "running", "update failed" + + assert len(await db.list_sessions()) == 1, "list count wrong" + + rng = {"session_id": "s-1", "hosts": [{"id": "dc01"}], "edges": [], "layout": {}} + await db.upsert_range("s-1", rng) + assert (await db.get_range("s-1"))["hosts"][0]["id"] == "dc01", "range round-trip failed" + + # cascade delete + await db.delete_session("s-1") + assert await db.get_session("s-1") is None, "session not deleted" + assert await db.get_range("s-1") is None, "range not cascade-deleted" + print("PASS test_session_and_range_crud") + finally: + await db.close() + + +async def test_event_seq_and_replay() -> None: + db, _ = await _fresh_db() + try: + await db.append_event("s-1", "user_message", {"content": "hi"}) + await db.append_event("s-1", "generation", {"content": "hello", "usage": {}}) + await db.append_event("s-1", "check_run", {"hosts_updated": 3}) + # different session has its own seq space + await db.append_event("s-2", "user_message", {"content": "other"}) + + evts = await db.get_events("s-1") + seqs = [e["seq"] for e in evts] + assert seqs == [1, 2, 3], f"seq not monotonic 1..3: {seqs}" + + # per-session isolation + assert [e["seq"] for e in await db.get_events("s-2")] == [1], "cross-session seq leak" + + # kind filter (chat-replay style) excludes system kinds, preserves order + chat = await db.get_events("s-1", kinds=["user_message", "generation"]) + assert [e["kind"] for e in chat] == ["user_message", "generation"], "kind filter wrong" + assert [e["seq"] for e in chat] == [1, 2], "kind filter broke ordering" + + # payload preserved + assert chat[0]["payload"]["content"] == "hi", "payload not preserved" + print("PASS test_event_seq_and_replay") + finally: + await db.close() + + +async def test_concurrent_writes_no_loss() -> None: + """N interleaved async writes must all persist with unique, gapless seqs.""" + db, _ = await _fresh_db() + try: + n = 100 + await asyncio.gather( + *[db.append_event("s-1", "generation", {"i": i}) for i in range(n)] + ) + evts = await db.get_events("s-1") + seqs = sorted(e["seq"] for e in evts) + assert len(evts) == n, f"lost writes: got {len(evts)} of {n}" + assert seqs == list(range(1, n + 1)), "seqs not unique/gapless under concurrency" + + # concurrent session upserts with distinct ids + await asyncio.gather( + *[db.upsert_session({"id": f"s-{i}", "status": "new"}) for i in range(n)] + ) + assert len(await db.list_sessions()) == n, "concurrent session upserts lost rows" + print("PASS test_concurrent_writes_no_loss") + finally: + await db.close() + + +async def _main() -> None: + await test_session_and_range_crud() + await test_event_seq_and_replay() + await test_concurrent_writes_no_loss() + print("ALL PASS") + + +if __name__ == "__main__": + asyncio.run(_main()) From 4781b79e4bef7be3b1e50d382a08517a2c117e49 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Mon, 3 Aug 2026 19:54:28 -0400 Subject: [PATCH 03/83] =?UTF-8?q?feat(webapp):=20Phase=202=20=E2=80=94=20s?= =?UTF-8?q?essions=20&=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add labconfig (snapshot derivation: provider/region file-level, variant/lab/network per-env; 3-way topology seeding from config.json; versioned yaml backup + write-new-env), SessionService (create/list/get/delete + create-new-env), and session lifecycle + RangeView REST endpoints. Unit tests for snapshot/seeding/backup and session service; TestClient tests for the REST layer. Co-Authored-By: Claude Opus 4.8 --- webapp/backend/labconfig.py | 179 +++++++++++++++++++++++ webapp/backend/paths.py | 9 +- webapp/backend/server.py | 110 ++++++++++++-- webapp/backend/sessions.py | 112 ++++++++++++++ webapp/backend/tests/test_labconfig.py | 129 ++++++++++++++++ webapp/backend/tests/test_server_rest.py | 79 ++++++++++ webapp/backend/tests/test_sessions.py | 127 ++++++++++++++++ 7 files changed, 732 insertions(+), 13 deletions(-) create mode 100644 webapp/backend/labconfig.py create mode 100644 webapp/backend/sessions.py create mode 100644 webapp/backend/tests/test_labconfig.py create mode 100644 webapp/backend/tests/test_server_rest.py create mode 100644 webapp/backend/tests/test_sessions.py diff --git a/webapp/backend/labconfig.py b/webapp/backend/labconfig.py new file mode 100644 index 00000000..de293626 --- /dev/null +++ b/webapp/backend/labconfig.py @@ -0,0 +1,179 @@ +"""Derive session snapshots and seed range topology from lab config (§4.3, §6.3). + +Two inputs: + - ``dreadgoad.yaml`` → the session *snapshot* (provider/region file-level; + variant/lab/network per-env) + - ``ad//data/config.json`` → the range *topology* (hosts + roles) + +The snapshot is a cache derived from the ``(config_path, env)`` anchor; the +topology is the config-seeded node set the ingestion hook later overlays. +""" + +from __future__ import annotations + +import json +import os +import shutil +import typing as t + +import yaml + +# config.json host `type` → RangeView role (§6.3). +_ROLE_BY_TYPE = { + "dc": "dc", + "server": "member", + "workstation": "workstation", +} + + +def derive_snapshot(config_path: str, env: str) -> dict[str, t.Any]: + """Build a session ``snapshot`` from ``(config_path, env)``. + + Provider/region are file-level (top of ``dreadgoad.yaml``); variant/lab/ + network come from the named env. Credentials are never included. + """ + with open(config_path) as f: + data = yaml.safe_load(f) or {} + + provider = data.get("provider") + region = data.get("region") + envs = data.get("environments") or {} + e = envs.get(env) or {} + + variant_target = e.get("variant_target") + variant_source = e.get("variant_source") + lab = variant_target or variant_source + + snapshot: dict[str, t.Any] = { + "provider": provider, + "region": region, + "lab": lab, + "variant_name": e.get("variant_name"), + "vpc_cidr": e.get("vpc_cidr"), + "attack_box": None, # discovered post-deploy + } + # Provider-specific block (selectors only, never secrets). + if provider == "aws": + snapshot["aws"] = {"profile": None} + elif provider == "azure": + snapshot["azure"] = { + "subscription_id": None, + "resource_group": None, + "ssh_key": None, + "ssh_user": "kali", + } + return snapshot + + +def _role_for(host_type: str) -> str: + return _ROLE_BY_TYPE.get((host_type or "").lower(), "other") + + +def _blank_dynamic() -> dict[str, t.Any]: + return { + "status": "unknown", + "health": "unknown", + "ip_private": None, + "ip_public": None, + "cloud_id": None, + "last_checked_at": None, + } + + +def seed_topology(lab_config_path: str | None, provider: str | None) -> dict[str, t.Any]: + """Seed a range's node set from ``config.json`` + infra nodes (§6.3). + + 3-way merge, v1 subset: + - **config** hosts from ``config.json`` (``type`` → role) + - **infra** nodes not in the lab config: attack box always; bastion for + Azure (SSM has no bastion node on AWS) + Extensions are added later by a re-seed when ``/extensions`` runs. + Edges are deferred (v1 nodes-only), so ``edges`` is empty. + + If ``lab_config_path`` is None or missing (greenfield range whose variant + isn't generated yet), only infra nodes are seeded; a later re-seed picks up + the config hosts once they exist. + """ + hosts_cfg: dict[str, t.Any] = {} + if lab_config_path and os.path.isfile(lab_config_path): + with open(lab_config_path) as f: + cfg = json.load(f) + hosts_cfg = (cfg.get("lab") or {}).get("hosts") or {} + + hosts: list[dict[str, t.Any]] = [] + for _key, h in hosts_cfg.items(): + hostname = h.get("hostname", _key) + host = { + "id": hostname, + "hostname": hostname, + "role": _role_for(h.get("type", "")), + "source": "config", + "domain": h.get("domain"), + **_blank_dynamic(), + } + hosts.append(host) + + # Infra nodes (not in the lab config). + hosts.append({ + "id": "attackbox", "hostname": "attackbox", "role": "attackbox", + "source": "infra", "domain": None, **_blank_dynamic(), + }) + if provider == "azure": + hosts.append({ + "id": "bastion", "hostname": "bastion", "role": "bastion", + "source": "infra", "domain": None, **_blank_dynamic(), + }) + + return {"hosts": hosts, "edges": [], "layout": {}, "last_checked_at": None} + + +def lab_config_path(repo_root: str, lab: str | None) -> str | None: + """Resolve ``ad//data/config.json`` under the repo root. + + ``lab`` is a repo-relative dir like ``ad/GOAD-dreadindex``. Returns None if + ``lab`` is unset. + """ + if not lab: + return None + return os.path.join(repo_root, lab, "data", "config.json") + + +def write_new_env( + config_path: str, + env_name: str, + env_fields: dict[str, t.Any], + top_level: dict[str, t.Any] | None = None, +) -> str: + """Add/replace an env entry in a ``dreadgoad.yaml`` (create-new flow, §4.3). + + Backs up the file first (if it exists). ``top_level`` sets file-level keys + (``provider``/``region``) shared by all envs. Note: round-tripping via + ``safe_dump`` does not preserve comments — the backup is the safety net. + """ + data: dict[str, t.Any] = {} + if os.path.isfile(config_path): + with open(config_path) as f: + data = yaml.safe_load(f) or {} + backup_yaml(config_path) + + if top_level: + data.update(top_level) + envs = data.setdefault("environments", {}) + envs[env_name] = env_fields + + with open(config_path, "w") as f: + yaml.safe_dump(data, f, sort_keys=False) + return config_path + + +def backup_yaml(config_path: str) -> str: + """Write a versioned backup copy of a yaml before mutating it (§4.3). + + Returns the backup path (``.bak.N`` with the next free N). + """ + n = 1 + while os.path.exists(f"{config_path}.bak.{n}"): + n += 1 + backup = f"{config_path}.bak.{n}" + shutil.copy2(config_path, backup) + return backup diff --git a/webapp/backend/paths.py b/webapp/backend/paths.py index dbbfe353..3c4df355 100644 --- a/webapp/backend/paths.py +++ b/webapp/backend/paths.py @@ -26,8 +26,13 @@ def repo_root() -> Path: def state_root() -> Path: - """``.dreadgoad/webapp/`` under the repo root, created if missing.""" - root = repo_root() / ".dreadgoad" / "webapp" + """``.dreadgoad/webapp/`` under the repo root, created if missing. + + Overridable via ``DREADGOAD_WEBAPP_STATE_ROOT`` (used by tests to isolate + the DB and session dirs from the repo). + """ + override = os.environ.get("DREADGOAD_WEBAPP_STATE_ROOT") + root = Path(override) if override else repo_root() / ".dreadgoad" / "webapp" root.mkdir(parents=True, exist_ok=True) return root diff --git a/webapp/backend/server.py b/webapp/backend/server.py index d3159f48..9d7291ca 100644 --- a/webapp/backend/server.py +++ b/webapp/backend/server.py @@ -1,45 +1,133 @@ """FastAPI backend entry point. -Phase 0: minimal shell — health + config endpoints and static frontend mount. -Later phases add: SQLite persistence (§6), session lifecycle REST (§7), -multiplexed chat + range-state WebSockets, the agent, and the ingestion hook. +Phase 0: health/config + static mount. +Phase 2: SQLite-backed session lifecycle REST + RangeView reads. +Later: multiplexed chat + range-state WebSockets, the agent, ingestion hook. """ from __future__ import annotations import os import typing as t +from contextlib import asynccontextmanager -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from fastapi.staticfiles import StaticFiles from . import __version__ as VERSION +from . import paths +from .db import Database +from .sessions import SessionService # Default model + provider (design decision: Sonnet 5 via OpenRouter). -_DEFAULT_MODEL = os.environ.get("DREADGOAD_WEBAPP_MODEL", "openrouter/anthropic/claude-sonnet-5") +_DEFAULT_MODEL = os.environ.get( + "DREADGOAD_WEBAPP_MODEL", "openrouter/anthropic/claude-sonnet-5" +) -app = FastAPI(title="DreadGOAD Web App") + +@asynccontextmanager +async def _lifespan(app: FastAPI) -> t.AsyncIterator[None]: + """Open the DB and build the session service on startup.""" + db = await Database(paths.resolve_db_path()).connect() + await db.set_meta("schema_version", 1) + app.state.db = db + app.state.sessions = SessionService( + db, repo_root=str(paths.repo_root()), sessions_root=paths.sessions_root() + ) + try: + yield + finally: + await db.close() + + +app = FastAPI(title="DreadGOAD Web App", lifespan=_lifespan) + + +# --- health / config ------------------------------------------------------- @app.get("/api/health") async def health() -> dict[str, t.Any]: - """Liveness probe used by the launcher and Phase-0 manual test.""" return {"status": "ok", "version": VERSION} @app.get("/api/config") async def get_config() -> dict[str, t.Any]: - """Static app config for the frontend shell.""" - return {"version": VERSION, "default_model": _DEFAULT_MODEL} + return { + "version": VERSION, + "default_model": _DEFAULT_MODEL, + "default_config_path": str(paths.repo_root() / "dreadgoad.yaml"), + } + + +# --- session lifecycle (§7) ------------------------------------------------ + + +def _svc(app: FastAPI) -> SessionService: + return app.state.sessions + + +@app.post("/api/sessions") +async def create_session(body: dict[str, t.Any]) -> dict[str, t.Any]: + """Create a session. Modes: ``attach`` (default) or ``new`` (write env).""" + svc = _svc(app) + mode = body.get("mode", "attach") + config_path = body.get("config_path") or str(paths.repo_root() / "dreadgoad.yaml") + env = (body.get("env") or "").strip() + if not env: + raise HTTPException(status_code=400, detail="env is required") + model = body.get("model") or _DEFAULT_MODEL + label = body.get("label") + + if mode == "new": + env_fields = body.get("env_fields") or {} + top_level = body.get("top_level") + return await svc.create_new_env_session( + config_path, env, env_fields, top_level=top_level, model=model, label=label + ) + return await svc.create_session(config_path, env, model=model, label=label) + + +@app.get("/api/sessions") +async def list_sessions() -> dict[str, t.Any]: + return {"sessions": await _svc(app).list_sessions()} + + +@app.get("/api/sessions/{session_id}") +async def get_session(session_id: str) -> dict[str, t.Any]: + s = await _svc(app).get_session(session_id) + if s is None: + raise HTTPException(status_code=404, detail="session not found") + return s + + +@app.delete("/api/sessions/{session_id}") +async def delete_session(session_id: str) -> dict[str, t.Any]: + ok = await _svc(app).delete_session(session_id) + if not ok: + raise HTTPException(status_code=404, detail="session not found") + return {"deleted": session_id} + + +# --- RangeView reads (§7) -------------------------------------------------- + + +@app.get("/api/ranges/{session_id}") +async def get_range(session_id: str) -> dict[str, t.Any]: + rng = await app.state.db.get_range(session_id) + if rng is None: + raise HTTPException(status_code=404, detail="range not found") + return rng + + +# --- static frontend ------------------------------------------------------- def mount_frontend(frontend_dist: str) -> None: - """Mount the built Vite frontend at ``/`` if the dist dir exists.""" if os.path.isdir(frontend_dist): app.mount("/", StaticFiles(directory=frontend_dist, html=True), name="frontend") -# The launcher sets this to the built dist dir when serving in production mode. _frontend_dist = os.environ.get("DREADGOAD_WEBAPP_FRONTEND_DIST") if _frontend_dist: mount_frontend(_frontend_dist) diff --git a/webapp/backend/sessions.py b/webapp/backend/sessions.py new file mode 100644 index 00000000..139ac647 --- /dev/null +++ b/webapp/backend/sessions.py @@ -0,0 +1,112 @@ +"""Session lifecycle service (design §4.2, §4.3, §7). + +A session = a ``(config_path, env)`` anchor + a derived snapshot + a working +dir. Create/list/get/delete over the SQLite layer; topology is seeded at +create time (config hosts if the lab exists, infra nodes otherwise). +""" + +from __future__ import annotations + +import re +import shutil +import typing as t +import uuid +from datetime import datetime, timezone +from pathlib import Path + +from . import labconfig +from .db import Database + + +def _slug(s: str) -> str: + return re.sub(r"[^a-z0-9]+", "-", (s or "").lower()).strip("-")[:40] or "session" + + +def _now() -> str: + return datetime.now(timezone.utc).isoformat() + + +class SessionService: + def __init__(self, db: Database, repo_root: str | Path, sessions_root: str | Path) -> None: + self.db = db + self.repo_root = str(repo_root) + self.sessions_root = Path(sessions_root) + + async def create_session( + self, + config_path: str, + env: str, + model: str | None = None, + label: str | None = None, + ) -> dict[str, t.Any]: + """Attach a session to an existing ``(config_path, env)``.""" + snap = labconfig.derive_snapshot(config_path, env) + + sid = "s-" + uuid.uuid4().hex[:8] + lbl = label or f"{env} · {snap.get('provider')}/{snap.get('variant_name') or env}" + dirname = f"{_slug(label or env)}-{sid[2:]}" + sdir = self.sessions_root / dirname + sdir.mkdir(parents=True, exist_ok=True) + + session = { + "id": sid, + "label": lbl, + "model": model, + "status": "new", + "anchor": {"config_path": str(config_path), "env": env}, + "snapshot": snap, + "session_dir": str(sdir), + "created_at": _now(), + "updated_at": _now(), + } + await self.db.upsert_session(session) + + topo = self._seed_topology(snap) + topo["session_id"] = sid + await self.db.upsert_range(sid, topo) + + await self.db.append_event(sid, "session_created", {"label": lbl}) + return session + + async def create_new_env_session( + self, + config_path: str, + env_name: str, + env_fields: dict[str, t.Any], + top_level: dict[str, t.Any] | None = None, + model: str | None = None, + label: str | None = None, + ) -> dict[str, t.Any]: + """Create-new-env flow: write the env into the yaml, then attach.""" + labconfig.write_new_env(config_path, env_name, env_fields, top_level) + return await self.create_session(config_path, env_name, model=model, label=label) + + def _seed_topology(self, snapshot: dict[str, t.Any]) -> dict[str, t.Any]: + cfg = labconfig.lab_config_path(self.repo_root, snapshot.get("lab")) + return labconfig.seed_topology(cfg, snapshot.get("provider")) + + async def list_sessions(self) -> list[dict[str, t.Any]]: + return await self.db.list_sessions() + + async def get_session(self, session_id: str) -> dict[str, t.Any] | None: + return await self.db.get_session(session_id) + + async def delete_session(self, session_id: str) -> bool: + """Delete a session, its range/events, and its working dir.""" + session = await self.db.get_session(session_id) + if session is None: + return False + await self.db.delete_session(session_id) + sdir = session.get("session_dir") + if sdir: + shutil.rmtree(sdir, ignore_errors=True) + return True + + async def set_status(self, session_id: str, status: str) -> None: + """Flush a status-critical write immediately (§6.1 durability).""" + session = await self.db.get_session(session_id) + if session is None: + return + session["status"] = status + session["updated_at"] = _now() + await self.db.upsert_session(session) diff --git a/webapp/backend/tests/test_labconfig.py b/webapp/backend/tests/test_labconfig.py new file mode 100644 index 00000000..733769bf --- /dev/null +++ b/webapp/backend/tests/test_labconfig.py @@ -0,0 +1,129 @@ +"""Tests for snapshot derivation, topology seeding, and yaml backup (Phase 2). + +Standalone: python webapp/backend/tests/test_labconfig.py +""" + +from __future__ import annotations + +import os +import pathlib +import sys +import tempfile + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[3])) + +from webapp.backend.labconfig import ( # noqa: E402 + backup_yaml, + derive_snapshot, + seed_topology, +) + +_REPO = pathlib.Path(__file__).resolve().parents[3] + +_FIXTURE_YAML = """\ +provider: azure +region: centralus +environments: + staging: + variant: true + variant_source: ad/GOAD + variant_target: ad/GOAD-dreadindex + variant_name: dreadindex + vpc_cidr: "10.1.0.0/16" +""" + +_FIXTURE_YAML_AWS = """\ +provider: aws +region: us-west-2 +environments: + dev: + variant_source: ad/GOAD + vpc_cidr: "10.0.0.0/16" +""" + + +def test_derive_snapshot_azure() -> None: + tmp = tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) + tmp.write(_FIXTURE_YAML) + tmp.close() + snap = derive_snapshot(tmp.name, "staging") + assert snap["provider"] == "azure", snap + assert snap["region"] == "centralus", snap + assert snap["lab"] == "ad/GOAD-dreadindex", "lab should be variant_target" + assert snap["variant_name"] == "dreadindex", snap + assert snap["vpc_cidr"] == "10.1.0.0/16", snap + assert snap["attack_box"] is None, "attack_box is discovered, not derived" + assert "azure" in snap and snap["azure"]["ssh_user"] == "kali", snap + assert "aws" not in snap, "no aws block for an azure session" + os.unlink(tmp.name) + print("PASS test_derive_snapshot_azure") + + +def test_derive_snapshot_aws_falls_back_to_source() -> None: + tmp = tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) + tmp.write(_FIXTURE_YAML_AWS) + tmp.close() + snap = derive_snapshot(tmp.name, "dev") + assert snap["provider"] == "aws", snap + # No variant_target → lab falls back to variant_source. + assert snap["lab"] == "ad/GOAD", snap + assert snap["aws"] == {"profile": None}, snap + os.unlink(tmp.name) + print("PASS test_derive_snapshot_aws_falls_back_to_source") + + +def test_seed_topology_from_goad_config() -> None: + cfg = _REPO / "ad" / "GOAD" / "data" / "config.json" + assert cfg.is_file(), f"missing fixture: {cfg}" + topo = seed_topology(str(cfg), provider="azure") + hosts = {h["id"]: h for h in topo["hosts"]} + + # kingslanding is a DC seeded from config. + assert "kingslanding" in hosts, hosts.keys() + assert hosts["kingslanding"]["role"] == "dc", hosts["kingslanding"] + assert hosts["kingslanding"]["source"] == "config", hosts["kingslanding"] + # dynamic fields start blank/unknown. + assert hosts["kingslanding"]["status"] == "unknown", hosts["kingslanding"] + assert hosts["kingslanding"]["cloud_id"] is None, hosts["kingslanding"] + + # a `server` type maps to role member (castelblack = srv02). + assert hosts["castelblack"]["role"] == "member", hosts["castelblack"] + + # infra nodes: attack box always; bastion for azure. + assert hosts["attackbox"]["source"] == "infra", "attackbox missing" + assert hosts["bastion"]["source"] == "infra", "azure should get a bastion node" + + # edges deferred in v1. + assert topo["edges"] == [], "edges should be empty (deferred)" + print("PASS test_seed_topology_from_goad_config") + + +def test_seed_topology_aws_has_no_bastion() -> None: + cfg = _REPO / "ad" / "GOAD" / "data" / "config.json" + topo = seed_topology(str(cfg), provider="aws") + ids = {h["id"] for h in topo["hosts"]} + assert "attackbox" in ids, "aws still has an attack box" + assert "bastion" not in ids, "aws (SSM) should not add a bastion node" + print("PASS test_seed_topology_aws_has_no_bastion") + + +def test_backup_yaml_versions() -> None: + tmp = tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False) + tmp.write("a: 1\n") + tmp.close() + b1 = backup_yaml(tmp.name) + b2 = backup_yaml(tmp.name) + assert b1.endswith(".bak.1") and b2.endswith(".bak.2"), (b1, b2) + assert os.path.isfile(b1) and os.path.isfile(b2), "backups not written" + for p in (tmp.name, b1, b2): + os.unlink(p) + print("PASS test_backup_yaml_versions") + + +if __name__ == "__main__": + test_derive_snapshot_azure() + test_derive_snapshot_aws_falls_back_to_source() + test_seed_topology_from_goad_config() + test_seed_topology_aws_has_no_bastion() + test_backup_yaml_versions() + print("ALL PASS") diff --git a/webapp/backend/tests/test_server_rest.py b/webapp/backend/tests/test_server_rest.py new file mode 100644 index 00000000..92c25e9f --- /dev/null +++ b/webapp/backend/tests/test_server_rest.py @@ -0,0 +1,79 @@ +"""REST endpoint tests via FastAPI TestClient (Phase 2: T2.2). + +Standalone: python webapp/backend/tests/test_server_rest.py +Requires fastapi + httpx (installed in the project venv). +""" + +from __future__ import annotations + +import pathlib +import sys +import tempfile + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[3])) + +_TMP = tempfile.mkdtemp(prefix="dg-rest-") +# Isolate DB + session dirs before importing the app. +import os # noqa: E402 + +os.environ["DREADGOAD_WEBAPP_STATE_ROOT"] = _TMP + +from fastapi.testclient import TestClient # noqa: E402 + +from webapp.backend.server import app # noqa: E402 + +_YAML = """\ +provider: aws +region: us-west-2 +environments: + dev: + variant_source: ad/GOAD + vpc_cidr: "10.0.0.0/16" +""" + + +def main() -> None: + cfg = pathlib.Path(_TMP) / "dreadgoad.yaml" + cfg.write_text(_YAML) + + with TestClient(app) as client: + # health + assert client.get("/api/health").json()["status"] == "ok" + + # create (attach) + r = client.post("/api/sessions", json={"config_path": str(cfg), "env": "dev"}) + assert r.status_code == 200, r.text + s = r.json() + sid = s["id"] + assert s["snapshot"]["provider"] == "aws", s + print("PASS create session") + + # missing env → 400 + assert client.post("/api/sessions", json={"config_path": str(cfg)}).status_code == 400 + print("PASS create requires env") + + # list + lst = client.get("/api/sessions").json()["sessions"] + assert any(x["id"] == sid for x in lst), lst + print("PASS list sessions") + + # get one + assert client.get(f"/api/sessions/{sid}").json()["id"] == sid + assert client.get("/api/sessions/nope").status_code == 404 + print("PASS get session + 404") + + # range read (seeded topology, infra-only since ad/GOAD is the base lab) + rng = client.get(f"/api/ranges/{sid}").json() + assert any(h["id"] == "attackbox" for h in rng["hosts"]), rng + print("PASS range read") + + # delete + assert client.delete(f"/api/sessions/{sid}").status_code == 200 + assert client.get(f"/api/sessions/{sid}").status_code == 404 + print("PASS delete session") + + print("ALL PASS") + + +if __name__ == "__main__": + main() diff --git a/webapp/backend/tests/test_sessions.py b/webapp/backend/tests/test_sessions.py new file mode 100644 index 00000000..00e17e02 --- /dev/null +++ b/webapp/backend/tests/test_sessions.py @@ -0,0 +1,127 @@ +"""Tests for SessionService (Phase 2: T2.2). + +Standalone: python webapp/backend/tests/test_sessions.py +""" + +from __future__ import annotations + +import asyncio +import os +import pathlib +import sys +import tempfile + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[3])) + +from webapp.backend.db import Database # noqa: E402 +from webapp.backend.sessions import SessionService # noqa: E402 + +_REPO = pathlib.Path(__file__).resolve().parents[3] + +_YAML = """\ +provider: azure +region: centralus +environments: + staging: + variant: true + variant_source: ad/GOAD + variant_target: ad/GOAD + variant_name: dreadindex + vpc_cidr: "10.1.0.0/16" +""" + + +async def _svc(tmp: pathlib.Path) -> SessionService: + db = await Database(str(tmp / "state.db")).connect() + return SessionService(db, repo_root=str(_REPO), sessions_root=tmp / "sessions") + + +async def test_create_attach_session() -> None: + with tempfile.TemporaryDirectory() as d: + tmp = pathlib.Path(d) + cfg = tmp / "dreadgoad.yaml" + cfg.write_text(_YAML) + svc = await _svc(tmp) + try: + s = await svc.create_session(str(cfg), "staging", model="m") + sid = s["id"] + + # session persisted with anchor + snapshot + got = await svc.get_session(sid) + assert got is not None and got["anchor"]["env"] == "staging", got + assert got["snapshot"]["provider"] == "azure", got + + # working dir created + assert os.path.isdir(got["session_dir"]), "session dir not created" + + # range seeded from ad/GOAD/data/config.json (variant_target=ad/GOAD) + rng = await svc.db.get_range(sid) + ids = {h["id"] for h in rng["hosts"]} + assert "kingslanding" in ids, ids + assert "attackbox" in ids and "bastion" in ids, "infra nodes missing" + + # session_created event recorded + evts = await svc.db.get_events(sid) + assert any(e["kind"] == "session_created" for e in evts), evts + + assert len(await svc.list_sessions()) == 1 + print("PASS test_create_attach_session") + finally: + await svc.db.close() + + +async def test_delete_session_removes_dir_and_rows() -> None: + with tempfile.TemporaryDirectory() as d: + tmp = pathlib.Path(d) + cfg = tmp / "dreadgoad.yaml" + cfg.write_text(_YAML) + svc = await _svc(tmp) + try: + s = await svc.create_session(str(cfg), "staging") + sdir = s["session_dir"] + assert os.path.isdir(sdir) + ok = await svc.delete_session(s["id"]) + assert ok, "delete returned False" + assert await svc.get_session(s["id"]) is None, "session row remains" + assert await svc.db.get_range(s["id"]) is None, "range row remains" + assert not os.path.isdir(sdir), "session dir not removed" + print("PASS test_delete_session_removes_dir_and_rows") + finally: + await svc.db.close() + + +async def test_create_new_env_writes_yaml_and_backs_up() -> None: + with tempfile.TemporaryDirectory() as d: + tmp = pathlib.Path(d) + cfg = tmp / "dreadgoad.yaml" + cfg.write_text(_YAML) + svc = await _svc(tmp) + try: + s = await svc.create_new_env_session( + str(cfg), + "prod", + env_fields={"variant_source": "ad/GOAD", "vpc_cidr": "10.9.0.0/16"}, + label="prod range", + ) + # backup written + assert (tmp / "dreadgoad.yaml.bak.1").is_file(), "no backup created" + # new env present in the yaml + import yaml + data = yaml.safe_load(cfg.read_text()) + assert "prod" in data["environments"], data["environments"].keys() + # session anchored to the new env + assert s["anchor"]["env"] == "prod", s + print("PASS test_create_new_env_writes_yaml_and_backs_up") + finally: + await svc.db.close() + + +async def _main() -> None: + await test_create_attach_session() + await test_delete_session_removes_dir_and_rows() + await test_create_new_env_writes_yaml_and_backs_up() + print("ALL PASS") + + +if __name__ == "__main__": + asyncio.run(_main()) From 5ef8df85ab6bfb58e795b88fd73ff7620154c00e Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Mon, 3 Aug 2026 20:02:20 -0400 Subject: [PATCH 04/83] =?UTF-8?q?feat(webapp):=20Phase=203=20=E2=80=94=20a?= =?UTF-8?q?gent,=20tools=20&=20commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the 14-command registry + provider-agnostic argv builder (injects --config/--env), a streaming CLI runner (cwd=repo root, SIGINT cancel), the per-session dreadgoad agent factory (Sonnet 5 via OpenRouter, fs-sandboxed, telemetry-free), and the multiplexed /ws/chat endpoint (direct-dispatch slash commands vs LLM free-text, event persistence + replay). Unit tests cover argv construction and the streaming runner; live chat is a manual test (needs OPENROUTER_API_KEY). Co-Authored-By: Claude Opus 4.8 --- webapp/backend/agent.py | 95 +++++++++++++++++++ webapp/backend/chat.py | 128 ++++++++++++++++++++++++++ webapp/backend/cli.py | 67 ++++++++++++++ webapp/backend/commands.py | 105 +++++++++++++++++++++ webapp/backend/server.py | 32 ++++++- webapp/backend/tests/test_commands.py | 95 +++++++++++++++++++ 6 files changed, 520 insertions(+), 2 deletions(-) create mode 100644 webapp/backend/agent.py create mode 100644 webapp/backend/chat.py create mode 100644 webapp/backend/cli.py create mode 100644 webapp/backend/commands.py create mode 100644 webapp/backend/tests/test_commands.py diff --git a/webapp/backend/agent.py b/webapp/backend/agent.py new file mode 100644 index 00000000..a6a9e700 --- /dev/null +++ b/webapp/backend/agent.py @@ -0,0 +1,95 @@ +"""Per-session dreadgoad agent factory (design §5). + +Adapted from ALFRED's agent: a ``LocalTaskAgent`` that bypasses platform +telemetry, sandboxes filesystem writes to the session working dir, and is +told how to drive the dreadgoad CLI for *this* session's range (its +``(config_path, env)`` anchor). Free-text prompts go here; deterministic +slash commands are dispatched directly (see server WS handler). +""" + +from __future__ import annotations + +import typing as t +from contextlib import AsyncExitStack, aclosing, asynccontextmanager +from copy import deepcopy + +import rigging as rg +from dreadnode.agent import TaskAgent +from dreadnode.agent.agent import CommitBehavior +from dreadnode.agent.events import AgentEvent +from dreadnode.agent.thread import Thread +from dreadnode.agent.tools.execute import command +from dreadnode.agent.tools.fs import Filesystem + + +class LocalTaskAgent(TaskAgent): + """TaskAgent that streams without platform telemetry (ALFRED pattern).""" + + _REMOVE_TOOLS = {"finish_task", "give_up_on_task", "update_todo"} + + def model_post_init(self, context: t.Any) -> None: + super().model_post_init(context) + self.tools = [tool for tool in self.tools if tool.name not in self._REMOVE_TOOLS] + self.stop_conditions = [c for c in self.stop_conditions if c.name != "stop_never"] + + @asynccontextmanager + async def stream( + self, + user_input: str, + *, + thread: Thread | None = None, + commit: CommitBehavior = "always", + ) -> t.AsyncIterator[t.AsyncGenerator[AgentEvent, None]]: + thread = thread or self.thread + messages = [*deepcopy(thread.messages), rg.Message("user", str(user_input))] + async with AsyncExitStack() as stack: + for tool_container in self.tools: + if hasattr(tool_container, "__aenter__") and hasattr(tool_container, "__aexit__"): + await stack.enter_async_context(tool_container) + async with aclosing(self._stream(thread, messages, commit=commit)) as events: + yield events + + +def _instructions(session: dict[str, t.Any], repo_root: str) -> str: + anchor = session["anchor"] + snap = session.get("snapshot", {}) + return f"""\ +You are the DreadGOAD range agent. You help build, manage, reset, and validate +one Active Directory lab range via the `dreadgoad` CLI. + +## This session's range +- Config file: {anchor['config_path']} +- Environment: {anchor['env']} +- Provider: {snap.get('provider')} Lab/variant: {snap.get('lab')} + +## Running the CLI +Always target THIS range by passing the anchor flags: + dreadgoad --config {anchor['config_path']} --env {anchor['env']} +Run CLI commands from the repo root ({repo_root}); the CLI reads ad/, infra/, +and dreadgoad.yaml from there. Provider is set in the config file — never pass +--provider. + +## Rules +- Your file workspace is the session directory; keep notes/artifacts there. +- Prefer the dedicated slash commands the operator has for common operations. +- Destructive actions (destroy, reset) change real cloud state — confirm intent. +- Report what you ran and the result concisely. +""" + + +def create_agent(model: str, session: dict[str, t.Any], repo_root: str) -> TaskAgent: + """Build a configured agent for a session. + + The LLM key must be in the environment (e.g. OPENROUTER_API_KEY). The + default model is Sonnet 5 via OpenRouter (see server config). + """ + session_dir = session.get("session_dir", ".") + fs = Filesystem(path=session_dir, variant="write") + return LocalTaskAgent( + name="dreadgoad-agent", + description="Builds, manages, and validates a DreadGOAD range", + model=model, + instructions=_instructions(session, repo_root), + max_steps=50, + tools=[command, fs], + ) diff --git a/webapp/backend/chat.py b/webapp/backend/chat.py new file mode 100644 index 00000000..3def03af --- /dev/null +++ b/webapp/backend/chat.py @@ -0,0 +1,128 @@ +"""Multiplexed chat WebSocket (design §5.1, §7). + +One socket carries a ``session_id`` on every message; the backend routes to +that session's agent (built lazily, kept per-session). Slash commands are +dispatched directly to the dreadgoad CLI (deterministic, streamed); free-text +is routed to the LLM agent. All events are persisted to the event log and +replayed on resume. + +Live behavior needs an LLM key (OPENROUTER_API_KEY); the structural wiring is +import-verifiable without one. +""" + +from __future__ import annotations + +import json +import typing as t + +from dreadnode.agent.events import ( + AgentEnd, + AgentError, + GenerationEnd, + ToolEnd, + ToolStart, +) + +from . import commands, paths +from .agent import create_agent +from .cli import start_command + +# Chat-kind events replayed on resume (§6.3). +CHAT_KINDS = ["user_message", "generation", "tool_start", "tool_end", "error", "agent_end"] + +# Per-session agent runtime (isolated; §4.2). Keyed by session id. +_agents: dict[str, t.Any] = {} + + +def format_event(event: t.Any) -> dict[str, t.Any] | None: + """Convert a dreadnode AgentEvent to a JSON-able chat event (ALFRED shape).""" + if isinstance(event, GenerationEnd): + usage = None + if event.usage: + usage = { + "input_tokens": event.usage.input_tokens, + "output_tokens": event.usage.output_tokens, + } + return {"kind": "generation", "content": event.message.content or "", "usage": usage} + if isinstance(event, ToolStart): + return {"kind": "tool_start", "tool": event.tool_call.name, + "args": event.tool_call.function.arguments} + if isinstance(event, ToolEnd): + return {"kind": "tool_end", "tool": event.tool_call.name, + "result": (event.message.content or "")[:2000]} + if isinstance(event, AgentError): + return {"kind": "error", "message": str(event.error)} + if isinstance(event, AgentEnd): + return {"kind": "agent_end", "failed": event.result.failed} + return None + + +async def _get_agent(app: t.Any, session_id: str) -> t.Any | None: + if session_id in _agents: + return _agents[session_id] + session = await app.state.db.get_session(session_id) + if session is None: + return None + agent = create_agent( + session.get("model") or "openrouter/anthropic/claude-sonnet-5", + session, + str(paths.repo_root()), + ) + _agents[session_id] = agent + return agent + + +async def handle_message(app: t.Any, ws: t.Any, session_id: str, content: str) -> None: + """Process one user message for a session: persist, dispatch, stream, persist.""" + db = app.state.db + + async def emit(kind: str, payload: dict[str, t.Any], *, persist: bool = True) -> None: + if persist: + await db.append_event(session_id, kind, payload) + await ws.send_text(json.dumps({"session_id": session_id, "kind": kind, **payload})) + + await emit("user_message", {"content": content}) + + # --- direct-dispatch slash commands --- + if commands.is_command(content): + name, extra = commands.parse_command(content) + session = await db.get_session(session_id) + argv = commands.build_argv(session, name, extra, repo_root=str(paths.repo_root())) + await emit("command_run", {"phase": "start", "command": name, "argv": argv}) + + rc = await start_command(argv, cwd=str(paths.repo_root())) + progress: list[str] = [] + exit_code, output = await rc.wait(on_line=progress.append) + + # command_progress is live-only (not persisted, §5.4). Phase 6 streams + # these as they arrive; v1 flushes the tail after completion. + for ln in progress[-100:]: + await emit("command_progress", {"line": ln}, persist=False) + + await emit("command_run", {"phase": "end", "command": name, + "exit_code": exit_code, "tail": output[-2000:]}) + await emit("agent_end", {"failed": exit_code != 0}) + return + + # --- free-text → LLM agent --- + agent = await _get_agent(app, session_id) + if agent is None: + await emit("error", {"message": "session not found"}) + await emit("agent_end", {"failed": True}) + return + try: + async with agent.stream(content) as events: + async for event in events: + formatted = format_event(event) + if formatted: + kind = formatted.pop("kind") + await emit(kind, formatted) + except Exception as exc: # noqa: BLE001 - surface any agent error to the client + await emit("error", {"message": f"agent error: {exc}"}) + await emit("agent_end", {"failed": True}) + + +async def replay(app: t.Any, ws: t.Any, session_id: str) -> None: + """Send chat-kind history for a session on (re)connect.""" + events = await app.state.db.get_events(session_id, kinds=CHAT_KINDS) + await ws.send_text(json.dumps({"session_id": session_id, "kind": "history", "events": events})) diff --git a/webapp/backend/cli.py b/webapp/backend/cli.py new file mode 100644 index 00000000..9396589c --- /dev/null +++ b/webapp/backend/cli.py @@ -0,0 +1,67 @@ +"""Runner that shells out to the dreadgoad CLI (design §5.1, §5.4). + +CLI commands run with ``cwd = repo root`` (they read ``ad/``, ``infra/``, +``dreadgoad.yaml``), streaming stdout line-by-line so long ops can surface a +live tail. The returned handle exposes cancellation (SIGINT) for §6. +""" + +from __future__ import annotations + +import asyncio +import signal +import typing as t +from pathlib import Path + +OnLine = t.Callable[[str], t.Any] + + +class RunningCommand: + """A live CLI subprocess with a streamed-output future and cancel().""" + + def __init__(self, proc: asyncio.subprocess.Process) -> None: + self._proc = proc + self.lines: list[str] = [] + + def cancel(self) -> None: + """Send SIGINT so the CLI can unwind gracefully (§5.4).""" + if self._proc.returncode is None: + with _suppress(): + self._proc.send_signal(signal.SIGINT) + + async def wait(self, on_line: OnLine | None = None) -> tuple[int, str]: + """Stream stdout (merged stderr) until exit; return (rc, full_output).""" + assert self._proc.stdout is not None + async for raw in self._proc.stdout: + line = raw.decode("utf-8", errors="replace").rstrip("\n") + self.lines.append(line) + if on_line is not None: + on_line(line) + await self._proc.wait() + return self._proc.returncode or 0, "\n".join(self.lines) + + +class _suppress: + def __enter__(self) -> None: + return None + + def __exit__(self, *exc: object) -> bool: + return True # swallow ProcessLookupError etc. + + +async def start_command(argv: list[str], cwd: str | Path) -> RunningCommand: + """Launch a CLI command (stdout+stderr merged) rooted at ``cwd``.""" + proc = await asyncio.create_subprocess_exec( + *argv, + cwd=str(cwd), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + ) + return RunningCommand(proc) + + +async def run_command( + argv: list[str], cwd: str | Path, on_line: OnLine | None = None +) -> tuple[int, str]: + """Convenience: start + wait. Returns (returncode, full_output).""" + rc = await start_command(argv, cwd) + return await rc.wait(on_line) diff --git a/webapp/backend/commands.py b/webapp/backend/commands.py new file mode 100644 index 00000000..43e48f3b --- /dev/null +++ b/webapp/backend/commands.py @@ -0,0 +1,105 @@ +"""Slash-command registry + dreadgoad argv builder (design §5). + +Each command maps to an exact CLI verb. Commands are provider-agnostic — the +session's ``(config_path, env)`` anchor is injected as global flags; provider +comes from the config file, so no ``--provider`` is needed. All CLI calls run +with ``cwd = repo root`` (see runner in cli.py). +""" + +from __future__ import annotations + +import shutil +import typing as t +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class Command: + name: str + verb: tuple[str, ...] # base CLI verb after `dreadgoad` + dispatch: str = "direct" # "direct" (deterministic) | "agent" + long_running: bool = False # streamed + guarded cancel (§5.4) + takes_args: bool = False + description: str = "" + + +# The 14 slash commands (§5.2). Dispatch defaults to "direct"; free-text +# prompts (not commands) are what route through the agent in v1. +REGISTRY: dict[str, Command] = { + "/up": Command("/up", ("up",), long_running=True, description="Full bring-up"), + "/provision": Command("/provision", ("provision",), long_running=True, description="Re-run config playbooks"), + "/reset": Command("/reset", ("lab", "reset"), long_running=True, description="Restore AD baseline"), + "/start": Command("/start", ("lab", "start"), description="Power on"), + "/stop": Command("/stop", ("lab", "stop"), description="Power off"), + "/destroy": Command("/destroy", ("infra", "destroy"), long_running=True, description="Tear down infra"), + "/instances": Command("/instances", ("lab", "status", "--json"), description="Cloud power state"), + "/health": Command("/health", ("health-check",), long_running=True, description="AD functional health"), + "/validate": Command("/validate", ("validate",), long_running=True, description="Vuln-config correctness"), + "/diagnose": Command("/diagnose", ("diagnose",), long_running=True, description="DC connectivity drill-down"), + "/score": Command("/score", ("score",), takes_args=True, description="Score an agent report"), + "/scrub": Command("/scrub", ("score", "reset"), description="Clean agent artifacts"), + "/variant": Command("/variant", ("variant", "generate"), takes_args=True, description="Generate a variant"), + "/extensions": Command("/extensions", ("extension",), takes_args=True, description="List/provision extensions"), +} + + +def resolve_bin(repo_root: str | Path) -> str: + """Locate the dreadgoad binary: PATH first, then ``cli/dreadgoad``.""" + found = shutil.which("dreadgoad") + if found: + return found + return str(Path(repo_root) / "cli" / "dreadgoad") + + +def _verb_for(cmd: Command, extra: list[str]) -> tuple[list[str], list[str]]: + """Resolve a command's concrete verb + trailing args from chat args. + + Handles the arg-shaped commands: + - /extensions → `extension list` (no arg) or `extension provision ` + - /score → `score --report ` (+ any flags like --live-verify) + - /variant → `variant generate ` + """ + if cmd.name == "/extensions": + if extra: + return ["extension", "provision", extra[0]], extra[1:] + return ["extension", "list"], [] + if cmd.name == "/score": + if extra: + return ["score", "--report", extra[0]], extra[1:] + return ["score"], [] + return list(cmd.verb), extra + + +def build_argv( + session: dict[str, t.Any], + name: str, + extra_args: list[str] | None = None, + repo_root: str | Path = ".", +) -> list[str]: + """Build the full dreadgoad argv for a command in a session's context. + + Shape: ``[bin, --config , --env , , ]``. + """ + if name not in REGISTRY: + raise KeyError(f"unknown command: {name}") + cmd = REGISTRY[name] + anchor = session["anchor"] + verb, trailing = _verb_for(cmd, list(extra_args or [])) + return [ + resolve_bin(repo_root), + "--config", str(anchor["config_path"]), + "--env", str(anchor["env"]), + *verb, + *trailing, + ] + + +def is_command(text: str) -> bool: + return text.strip().split(" ", 1)[0] in REGISTRY + + +def parse_command(text: str) -> tuple[str, list[str]]: + """Split ``/cmd arg1 arg2`` → ("/cmd", ["arg1", "arg2"]).""" + parts = text.strip().split() + return parts[0], parts[1:] diff --git a/webapp/backend/server.py b/webapp/backend/server.py index 9d7291ca..bec8ae8f 100644 --- a/webapp/backend/server.py +++ b/webapp/backend/server.py @@ -7,15 +7,16 @@ from __future__ import annotations +import json import os import typing as t from contextlib import asynccontextmanager -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect from fastapi.staticfiles import StaticFiles from . import __version__ as VERSION -from . import paths +from . import chat, paths from .db import Database from .sessions import SessionService @@ -120,6 +121,33 @@ async def get_range(session_id: str) -> dict[str, t.Any]: return rng +# --- multiplexed chat WebSocket (§7) --------------------------------------- + + +@app.websocket("/ws/chat") +async def ws_chat(websocket: WebSocket) -> None: + """One socket for all tabs; each message carries its ``session_id``.""" + await websocket.accept() + try: + while True: + raw = await websocket.receive_text() + try: + msg = json.loads(raw) + except json.JSONDecodeError: + continue + session_id = msg.get("session_id") + if not session_id: + continue + if msg.get("type") == "resume": + await chat.replay(app, websocket, session_id) + continue + content = (msg.get("content") or "").strip() + if content: + await chat.handle_message(app, websocket, session_id, content) + except WebSocketDisconnect: + return + + # --- static frontend ------------------------------------------------------- diff --git a/webapp/backend/tests/test_commands.py b/webapp/backend/tests/test_commands.py new file mode 100644 index 00000000..73f5ea0f --- /dev/null +++ b/webapp/backend/tests/test_commands.py @@ -0,0 +1,95 @@ +"""Tests for the command registry, argv builder, and CLI runner (Phase 3). + +Standalone: python webapp/backend/tests/test_commands.py +""" + +from __future__ import annotations + +import asyncio +import pathlib +import stat +import sys +import tempfile + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[3])) + +from webapp.backend import commands # noqa: E402 +from webapp.backend.cli import run_command # noqa: E402 + +_SESSION = {"anchor": {"config_path": "/x/dreadgoad.yaml", "env": "dev"}} + + +def _argv(name: str, extra: list[str] | None = None) -> list[str]: + return commands.build_argv(_SESSION, name, extra, repo_root="/repo") + + +def test_argv_injects_config_and_env() -> None: + a = _argv("/up") + assert a[0].endswith("dreadgoad"), a + assert a[1:5] == ["--config", "/x/dreadgoad.yaml", "--env", "dev"], a + assert a[5:] == ["up"], a + print("PASS test_argv_injects_config_and_env") + + +def test_argv_multiword_and_flag_verbs() -> None: + assert _argv("/reset")[5:] == ["lab", "reset"] + assert _argv("/instances")[5:] == ["lab", "status", "--json"] + assert _argv("/scrub")[5:] == ["score", "reset"] + print("PASS test_argv_multiword_and_flag_verbs") + + +def test_argv_arg_shaped_commands() -> None: + # /extensions: list vs provision + assert _argv("/extensions")[5:] == ["extension", "list"] + assert _argv("/extensions", ["elk"])[5:] == ["extension", "provision", "elk"] + # /score: report path + passthrough flag + assert _argv("/score", ["/tmp/r.jsonl", "--live-verify"])[5:] == [ + "score", "--report", "/tmp/r.jsonl", "--live-verify", + ] + # /variant: passthrough + assert _argv("/variant", ["--name", "v2"])[5:] == ["variant", "generate", "--name", "v2"] + print("PASS test_argv_arg_shaped_commands") + + +def test_registry_flags_and_parsing() -> None: + assert commands.REGISTRY["/up"].long_running is True + assert commands.REGISTRY["/instances"].long_running is False + assert commands.REGISTRY["/destroy"].verb == ("infra", "destroy") + assert commands.is_command("/health") and not commands.is_command("hello there") + assert commands.parse_command("/score /tmp/r.jsonl --live-verify") == ( + "/score", ["/tmp/r.jsonl", "--live-verify"], + ) + try: + _argv("/nope") + raise AssertionError("expected KeyError for unknown command") + except KeyError: + pass + print("PASS test_registry_flags_and_parsing") + + +async def test_runner_streams_and_returns_rc() -> None: + """Runner streams lines and reports the exit code (stubbed CLI).""" + with tempfile.TemporaryDirectory() as d: + stub = pathlib.Path(d) / "fakecli.sh" + stub.write_text("#!/usr/bin/env bash\necho line-one\necho line-two\nexit 3\n") + stub.chmod(stub.stat().st_mode | stat.S_IEXEC) + + seen: list[str] = [] + rc, out = await run_command([str(stub)], cwd=d, on_line=seen.append) + assert rc == 3, f"rc={rc}" + assert seen == ["line-one", "line-two"], seen + assert "line-one" in out and "line-two" in out, out + print("PASS test_runner_streams_and_returns_rc") + + +def main() -> None: + test_argv_injects_config_and_env() + test_argv_multiword_and_flag_verbs() + test_argv_arg_shaped_commands() + test_registry_flags_and_parsing() + asyncio.run(test_runner_streams_and_returns_rc()) + print("ALL PASS") + + +if __name__ == "__main__": + main() From 94cb9dc91408357e0d5b5da7c79745d05e3530f9 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Mon, 3 Aug 2026 20:05:02 -0400 Subject: [PATCH 05/83] =?UTF-8?q?feat(webapp):=20Phase=204=20=E2=80=94=20i?= =?UTF-8?q?ngestion=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the post-command hook: map_range_status overlays live instance state (from lab status --json) onto config-seeded hosts — matched hosts get status/ip/cloud_id, unmatched config hosts go absent, unmatched instances ignored; infra nodes match via aliases (kali→attackbox). run_check wires it to CLI+DB with stale-preserving failure handling; fired after every command in the chat flow and surfaced as an inline check_run. 6 mapping unit tests. Co-Authored-By: Claude Opus 4.8 --- webapp/backend/chat.py | 9 ++- webapp/backend/hook.py | 123 ++++++++++++++++++++++++++++++ webapp/backend/tests/test_hook.py | 105 +++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 webapp/backend/hook.py create mode 100644 webapp/backend/tests/test_hook.py diff --git a/webapp/backend/chat.py b/webapp/backend/chat.py index 3def03af..41b3a798 100644 --- a/webapp/backend/chat.py +++ b/webapp/backend/chat.py @@ -23,7 +23,7 @@ ToolStart, ) -from . import commands, paths +from . import commands, hook, paths from .agent import create_agent from .cli import start_command @@ -81,6 +81,11 @@ async def emit(kind: str, payload: dict[str, t.Any], *, persist: bool = True) -> await db.append_event(session_id, kind, payload) await ws.send_text(json.dumps({"session_id": session_id, "kind": kind, **payload})) + async def run_hook_and_emit() -> None: + """Fire the ingestion hook, surface check_run inline (§6.4).""" + payload = await hook.run_check(app, session_id) + await emit("check_run", payload) + await emit("user_message", {"content": content}) # --- direct-dispatch slash commands --- @@ -101,6 +106,7 @@ async def emit(kind: str, payload: dict[str, t.Any], *, persist: bool = True) -> await emit("command_run", {"phase": "end", "command": name, "exit_code": exit_code, "tail": output[-2000:]}) + await run_hook_and_emit() await emit("agent_end", {"failed": exit_code != 0}) return @@ -117,6 +123,7 @@ async def emit(kind: str, payload: dict[str, t.Any], *, persist: bool = True) -> if formatted: kind = formatted.pop("kind") await emit(kind, formatted) + await run_hook_and_emit() except Exception as exc: # noqa: BLE001 - surface any agent error to the client await emit("error", {"message": f"agent error: {exc}"}) await emit("agent_end", {"failed": True}) diff --git a/webapp/backend/hook.py b/webapp/backend/hook.py new file mode 100644 index 00000000..590df231 --- /dev/null +++ b/webapp/backend/hook.py @@ -0,0 +1,123 @@ +"""Post-command ingestion hook (design §6.2, §6.4). + +After a command, discover live instances (via ``lab status --json``) and +overlay their cloud state onto the range's config-seeded hosts. Overlay-only: +config hosts with no live instance go ``absent``; live instances with no +config host are ignored. The mapping is pure and unit-tested; ``run_check`` +wires it to the CLI + DB. +""" + +from __future__ import annotations + +import json +import typing as t +from datetime import datetime, timezone + +from . import commands, paths +from .cli import run_command + +# Cloud power state → our host.status enum (§6.3). +_STATE = { + "running": "running", + "stopped": "stopped", + "deallocated": "stopped", + "pending": "provisioning", + "starting": "provisioning", + "creating": "provisioning", + "terminated": "absent", +} + +# Infra nodes correlate via aliases (the cloud instance name differs from the +# node id, e.g. the attack box VM is named "…kali…"). +_ALIASES = { + "attackbox": ["attackbox", "kali", "attack"], + "bastion": ["bastion"], +} + + +def _now() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _norm_state(state: str | None) -> str: + return _STATE.get((state or "").lower(), "unknown") + + +def _match(host: dict[str, t.Any], instances: list[dict[str, t.Any]]) -> dict[str, t.Any] | None: + """Find the instance whose name contains the host id (or an alias).""" + hid = str(host["id"]).lower() + aliases = _ALIASES.get(hid, [hid]) + for inst in instances: + name = str(inst.get("name") or "").lower() + if any(a in name for a in aliases): + return inst + return None + + +def map_range_status( + rng: dict[str, t.Any], instances: list[dict[str, t.Any]], now: str | None = None +) -> dict[str, t.Any]: + """Overlay live instance state onto range hosts (pure; §6.4). + + Returns a new range doc. Matched hosts get status/ip/cloud_id refreshed; + unmatched config hosts go ``absent``. Unmatched instances are ignored. + """ + now = now or _now() + hosts_out: list[dict[str, t.Any]] = [] + for host in rng.get("hosts", []): + h = dict(host) + inst = _match(host, instances) + if inst is None: + h["status"] = "absent" + else: + h["status"] = _norm_state(inst.get("state")) + h["ip_private"] = inst.get("private_ip") or h.get("ip_private") + h["cloud_id"] = inst.get("id") or h.get("cloud_id") + h["last_checked_at"] = now + hosts_out.append(h) + out = dict(rng) + out["hosts"] = hosts_out + out["last_checked_at"] = now + return out + + +def summarize_changes( + before: dict[str, t.Any], after: dict[str, t.Any] +) -> dict[str, t.Any]: + """Build a check_run payload: which hosts changed status.""" + prev = {h["id"]: h.get("status") for h in before.get("hosts", [])} + changes = [] + for h in after.get("hosts", []): + old = prev.get(h["id"]) + if old != h.get("status"): + changes.append({"id": h["id"], "from": old, "to": h.get("status")}) + return {"hosts_updated": len(changes), "changes": changes} + + +async def run_check(app: t.Any, session_id: str) -> dict[str, t.Any]: + """Discover live state and overlay it onto the range (§6.4 flow). + + On failure: leave the range untouched (stale), don't advance + ``last_checked_at``, mark the session ``error``, and return an error + check_run payload. + """ + db = app.state.db + session = await db.get_session(session_id) + rng = await db.get_range(session_id) + if session is None or rng is None: + return {"error": "session/range not found"} + + argv = commands.build_argv(session, "/instances", repo_root=str(paths.repo_root())) + try: + rc, output = await run_command(argv, cwd=str(paths.repo_root())) + if rc != 0: + raise RuntimeError(f"lab status --json exited {rc}: {output[-500:]}") + instances = json.loads(output) + except Exception as exc: # noqa: BLE001 + await app.state.sessions.set_status(session_id, "error") + return {"error": str(exc)} + + updated = map_range_status(rng, instances) + payload = summarize_changes(rng, updated) + await db.upsert_range(session_id, updated) + return payload diff --git a/webapp/backend/tests/test_hook.py b/webapp/backend/tests/test_hook.py new file mode 100644 index 00000000..c3e608fe --- /dev/null +++ b/webapp/backend/tests/test_hook.py @@ -0,0 +1,105 @@ +"""Tests for the ingestion hook mapping (Phase 4: T4.1). + +Standalone: python webapp/backend/tests/test_hook.py +""" + +from __future__ import annotations + +import pathlib +import sys + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[3])) + +from webapp.backend.hook import map_range_status, summarize_changes # noqa: E402 + + +def _range() -> dict: + def host(hid, role, source="config"): + return {"id": hid, "hostname": hid, "role": role, "source": source, + "status": "unknown", "health": "unknown", "ip_private": None, + "ip_public": None, "cloud_id": None, "last_checked_at": None} + return { + "session_id": "s-1", + "hosts": [ + host("kingslanding", "dc"), + host("winterfell", "dc"), + host("attackbox", "attackbox", "infra"), + ], + "edges": [], "layout": {}, "last_checked_at": None, + } + + +def test_matched_host_gets_state_ip_id() -> None: + instances = [ + {"name": "goad-dreadgoad-kingslanding-vm", "id": "i-0abc", + "state": "running", "private_ip": "10.0.4.124"}, + ] + out = map_range_status(_range(), instances, now="T") + hosts = {h["id"]: h for h in out["hosts"]} + k = hosts["kingslanding"] + assert k["status"] == "running", k + assert k["ip_private"] == "10.0.4.124" and k["cloud_id"] == "i-0abc", k + assert k["last_checked_at"] == "T", k + print("PASS test_matched_host_gets_state_ip_id") + + +def test_unmatched_config_host_is_absent() -> None: + instances = [{"name": "x-kingslanding-vm", "id": "i-1", "state": "running", "private_ip": "1.2.3.4"}] + out = map_range_status(_range(), instances, now="T") + hosts = {h["id"]: h for h in out["hosts"]} + assert hosts["winterfell"]["status"] == "absent", "no instance → absent" + print("PASS test_unmatched_config_host_is_absent") + + +def test_unmatched_instance_ignored_no_new_node() -> None: + instances = [{"name": "some-random-elk-vm", "id": "i-9", "state": "running", "private_ip": "9.9.9.9"}] + out = map_range_status(_range(), instances, now="T") + assert len(out["hosts"]) == 3, "hook must not invent nodes" + print("PASS test_unmatched_instance_ignored_no_new_node") + + +def test_infra_alias_matches_kali() -> None: + instances = [{"name": "goad-dreadgoad-kali", "id": "i-kali", "state": "running", "private_ip": "10.0.4.9"}] + out = map_range_status(_range(), instances, now="T") + hosts = {h["id"]: h for h in out["hosts"]} + assert hosts["attackbox"]["status"] == "running", "attackbox should match the kali VM" + assert hosts["attackbox"]["cloud_id"] == "i-kali", hosts["attackbox"] + print("PASS test_infra_alias_matches_kali") + + +def test_state_normalization() -> None: + for cloud, expected in [("running", "running"), ("stopped", "stopped"), + ("deallocated", "stopped"), ("pending", "provisioning"), + ("terminated", "absent"), ("weird", "unknown")]: + instances = [{"name": "x-kingslanding-y", "id": "i", "state": cloud, "private_ip": ""}] + out = map_range_status(_range(), instances, now="T") + st = {h["id"]: h for h in out["hosts"]}["kingslanding"]["status"] + assert st == expected, f"{cloud} → {st}, expected {expected}" + print("PASS test_state_normalization") + + +def test_summarize_changes() -> None: + before = _range() + after = map_range_status(before, [ + {"name": "x-kingslanding-y", "id": "i", "state": "running", "private_ip": ""}, + ], now="T") + diff = summarize_changes(before, after) + # kingslanding unknown→running, winterfell unknown→absent, attackbox unknown→absent + assert diff["hosts_updated"] == 3, diff + ids = {c["id"] for c in diff["changes"]} + assert "kingslanding" in ids, diff + print("PASS test_summarize_changes") + + +def main() -> None: + test_matched_host_gets_state_ip_id() + test_unmatched_config_host_is_absent() + test_unmatched_instance_ignored_no_new_node() + test_infra_alias_matches_kali() + test_state_normalization() + test_summarize_changes() + print("ALL PASS") + + +if __name__ == "__main__": + main() From cc4e1f79db17355bdf947147f5e28c023d0f1d7a Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Mon, 3 Aug 2026 20:09:41 -0400 Subject: [PATCH 06/83] =?UTF-8?q?feat(webapp):=20Phase=205=20=E2=80=94=20f?= =?UTF-8?q?rontend=20(shell,=20tabs,=20RangeView)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React SPA: two-pane shell + resizer, session tab bar + new-session modal, TerminalChat over a single multiplexed WebSocket (routes events per session_id, background tabs stay live), and RangeView (React Flow nodes by role + status/health badges, persisted layout). Adds the layout-persist REST endpoint. Builds clean under strict TS + vite; visual/interactive behavior is the manual test. Co-Authored-By: Claude Opus 4.8 --- webapp/backend/server.py | 11 + webapp/frontend/package-lock.json | 3570 +++++++++++++++++ webapp/frontend/src/App.tsx | 196 +- webapp/frontend/src/api.ts | 41 + webapp/frontend/src/components/RangeView.tsx | 137 + .../frontend/src/components/TerminalChat.tsx | 112 + webapp/frontend/src/hooks/useWebSocket.ts | 61 + webapp/frontend/src/types.ts | 66 + 8 files changed, 4176 insertions(+), 18 deletions(-) create mode 100644 webapp/frontend/package-lock.json create mode 100644 webapp/frontend/src/api.ts create mode 100644 webapp/frontend/src/components/RangeView.tsx create mode 100644 webapp/frontend/src/components/TerminalChat.tsx create mode 100644 webapp/frontend/src/hooks/useWebSocket.ts create mode 100644 webapp/frontend/src/types.ts diff --git a/webapp/backend/server.py b/webapp/backend/server.py index bec8ae8f..f8c1d9c1 100644 --- a/webapp/backend/server.py +++ b/webapp/backend/server.py @@ -121,6 +121,17 @@ async def get_range(session_id: str) -> dict[str, t.Any]: return rng +@app.put("/api/ranges/{session_id}/layout") +async def save_layout(session_id: str, body: dict[str, t.Any]) -> dict[str, t.Any]: + """Persist per-range node positions (RangeView drag; §4.4).""" + rng = await app.state.db.get_range(session_id) + if rng is None: + raise HTTPException(status_code=404, detail="range not found") + rng["layout"] = body.get("layout", {}) + await app.state.db.upsert_range(session_id, rng) + return {"ok": True} + + # --- multiplexed chat WebSocket (§7) --------------------------------------- diff --git a/webapp/frontend/package-lock.json b/webapp/frontend/package-lock.json new file mode 100644 index 00000000..8ee87a38 --- /dev/null +++ b/webapp/frontend/package-lock.json @@ -0,0 +1,3570 @@ +{ + "name": "dreadgoad-webapp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dreadgoad-webapp", + "version": "0.1.0", + "dependencies": { + "@xyflow/react": "^12.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.6.3", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@xyflow/react": { + "version": "12.11.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.2.tgz", + "integrity": "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.79", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.79", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.79.tgz", + "integrity": "sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", + "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "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.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.399", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.399.tgz", + "integrity": "sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "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.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/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/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/webapp/frontend/src/App.tsx b/webapp/frontend/src/App.tsx index 05214f9d..426a3d56 100644 --- a/webapp/frontend/src/App.tsx +++ b/webapp/frontend/src/App.tsx @@ -1,30 +1,190 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' +import TerminalChat from './components/TerminalChat' +import RangeView from './components/RangeView' +import { useWebSocket } from './hooks/useWebSocket' +import { api, type AppConfig } from './api' +import type { ChatEvent, Session } from './types' + +const MIN_W = 320 +const DEFAULT_RATIO = 0.45 -// Phase 0 placeholder shell. Phase 5 replaces this with the two-pane -// layout (TerminalChat + RangeView) and the session tab bar. export default function App() { - const [version, setVersion] = useState('') - const [ok, setOk] = useState(null) + const [sessions, setSessions] = useState([]) + const [activeId, setActiveId] = useState(null) + const [msgs, setMsgs] = useState>({}) + const [cfg, setCfg] = useState(null) + const [ratio, setRatio] = useState(DEFAULT_RATIO) + const [showNew, setShowNew] = useState(false) + + const sessionsRef = useRef([]) + const resumedRef = useRef>(new Set()) + const containerRef = useRef(null) + sessionsRef.current = sessions + + // --- WebSocket (single, multiplexed by session_id) --- + const handleMessage = useCallback((data: string) => { + let ev: ChatEvent + try { ev = JSON.parse(data) } catch { return } + const sid = ev.session_id + if (!sid) return + if (ev.kind === 'history') { + const events = ev.events || [] + setMsgs(prev => ({ ...prev, [sid]: events })) + return + } + setMsgs(prev => ({ ...prev, [sid]: [...(prev[sid] || []), ev] })) + }, []) + + const resume = useCallback((send: (d: string) => void, id: string) => { + if (resumedRef.current.has(id)) return + resumedRef.current.add(id) + send(JSON.stringify({ type: 'resume', session_id: id })) + }, []) + + const handleOpen = useCallback((send: (d: string) => void) => { + // Re-subscribe every known session so background tabs stay live (§4.2). + resumedRef.current.clear() + for (const s of sessionsRef.current) resume(send, s.id) + }, [resume]) + + const { status, send } = useWebSocket('/ws/chat', handleMessage, handleOpen) + // --- load config + sessions --- useEffect(() => { - fetch('/api/health') - .then(r => r.json()) - .then(d => { setOk(d.status === 'ok'); setVersion(d.version || '') }) - .catch(() => setOk(false)) + api.config().then(setCfg).catch(() => {}) + api.listSessions().then(d => setSessions(d.sessions)).catch(() => {}) + }, []) + + // resume + activate a session + const activate = useCallback((id: string) => { + setActiveId(id) + if (status === 'connected') resume(send, id) + }, [status, send, resume]) + + useEffect(() => { + if (!activeId && sessions.length) activate(sessions[0].id) + }, [sessions, activeId, activate]) + + const sendMessage = useCallback((content: string) => { + if (!activeId) return + send(JSON.stringify({ session_id: activeId, content })) + }, [activeId, send]) + + const createSession = useCallback(async (body: Record) => { + const s = await api.createSession(body) + setSessions(prev => [...prev, s]) + setShowNew(false) + activate(s.id) + }, [activate]) + + const closeSession = useCallback(async (id: string) => { + await api.deleteSession(id).catch(() => {}) + setSessions(prev => prev.filter(s => s.id !== id)) + setMsgs(prev => { const n = { ...prev }; delete n[id]; return n }) + if (activeId === id) setActiveId(null) + }, [activeId]) + + // --- resizer --- + const onDrag = useCallback((e: React.MouseEvent) => { + e.preventDefault() + const move = (ev: MouseEvent) => { + const rect = containerRef.current?.getBoundingClientRect() + if (!rect) return + const r = Math.max(MIN_W / rect.width, Math.min(1 - MIN_W / rect.width, (ev.clientX - rect.left) / rect.width)) + setRatio(r) + } + const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up) } + document.addEventListener('mousemove', move) + document.addEventListener('mouseup', up) }, []) return ( -
-
- DreadGOAD +
+ {showNew && cfg && ( + setShowNew(false)} onCreate={createSession} /> + )} + + {/* Tab bar */} +
+ DreadGOAD + {sessions.map(s => ( +
activate(s.id)} style={{ + display: 'flex', alignItems: 'center', gap: 6, padding: '4px 10px', cursor: 'pointer', + borderRadius: 4, fontSize: 12, + background: s.id === activeId ? 'var(--dn-surface)' : 'transparent', + color: s.id === activeId ? 'var(--dn-text-bright)' : 'var(--dn-text-muted)', + }}> + {s.label} + { e.stopPropagation(); closeSession(s.id) }} style={{ color: 'var(--dn-text-dim)' }}>✕ +
+ ))} + +
+ + {/* Two-pane */} +
+
+ +
+
+
+ +
-
- {ok === null ? 'connecting…' : ok ? `backend online · v${version}` : 'backend offline'} +
+ ) +} + +function NewSessionModal({ cfg, onClose, onCreate }: { + cfg: AppConfig + onClose: () => void + onCreate: (body: Record) => void +}) { + const [configPath, setConfigPath] = useState(cfg.default_config_path) + const [env, setEnv] = useState('') + const [label, setLabel] = useState('') + + return ( +
+
e.stopPropagation()} style={{ background: 'var(--dn-surface)', border: '1px solid var(--dn-border-lt)', borderRadius: 6, padding: 20, width: 380, fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--dn-text)' }}> +
New Session
+ + + +
+ + +
) } + +function Field({ label, value, onChange, placeholder }: { label: string; value: string; onChange: (v: string) => void; placeholder?: string }) { + return ( +
+ + onChange(e.target.value)} style={{ + width: '100%', boxSizing: 'border-box', padding: '6px 8px', background: 'var(--dn-bg)', + border: '1px solid var(--dn-border)', borderRadius: 3, color: 'var(--dn-text)', + fontFamily: 'var(--font-mono)', fontSize: 12, + }} /> +
+ ) +} + +function btnStyle(primary: boolean): React.CSSProperties { + return { + background: primary ? 'var(--dg-brand)' : 'transparent', + border: primary ? 'none' : '1px solid var(--dn-border-lt)', + color: primary ? 'var(--dn-black)' : 'var(--dn-text-dim)', + fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: primary ? 700 : 400, + padding: '4px 12px', borderRadius: 3, cursor: 'pointer', + } +} diff --git a/webapp/frontend/src/api.ts b/webapp/frontend/src/api.ts new file mode 100644 index 00000000..8db75dda --- /dev/null +++ b/webapp/frontend/src/api.ts @@ -0,0 +1,41 @@ +// REST client for session lifecycle + RangeView reads (design §7). + +import type { RangeDoc, Session } from './types' + +async function json(res: Response): Promise { + if (!res.ok) throw new Error(`${res.status} ${await res.text()}`) + return res.json() as Promise +} + +export interface AppConfig { + version: string + default_model: string + default_config_path: string +} + +export const api = { + config: (): Promise => fetch('/api/config').then(r => json(r)), + + listSessions: (): Promise<{ sessions: Session[] }> => + fetch('/api/sessions').then(r => json(r)), + + createSession: (body: Record): Promise => + fetch('/api/sessions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }).then(r => json(r)), + + deleteSession: (id: string): Promise => + fetch(`/api/sessions/${id}`, { method: 'DELETE' }).then(r => json(r)), + + getRange: (id: string): Promise => + fetch(`/api/ranges/${id}`).then(r => json(r)), + + saveLayout: (id: string, layout: Record): Promise => + fetch(`/api/ranges/${id}/layout`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ layout }), + }).then(r => json(r)), +} diff --git a/webapp/frontend/src/components/RangeView.tsx b/webapp/frontend/src/components/RangeView.tsx new file mode 100644 index 00000000..e4698ca5 --- /dev/null +++ b/webapp/frontend/src/components/RangeView.tsx @@ -0,0 +1,137 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { + ReactFlow, + Background, + Controls, + useNodesState, + type Node, + type NodeProps, + type NodeChange, +} from '@xyflow/react' +import '@xyflow/react/dist/style.css' +import { api } from '../api' +import type { RangeDoc, RangeHost } from '../types' + +const ROLE_ICON: Record = { + dc: '🏰', member: '🖥️', workstation: '💻', bastion: '🛡️', + attackbox: '☠️', linux: '🐧', other: '❔', +} +const STATUS_COLOR: Record = { + running: 'var(--dn-success)', stopped: 'var(--dn-text-muted)', + provisioning: 'var(--dn-warning)', absent: 'var(--dn-error)', + unknown: 'var(--dn-text-dim)', +} + +function HostNode({ data }: NodeProps) { + const h = data as unknown as RangeHost + const color = STATUS_COLOR[h.status] ?? STATUS_COLOR.unknown + return ( +
+
+ {ROLE_ICON[h.role] ?? ROLE_ICON.other} + {h.hostname} +
+
+ {h.role}{h.domain ? ` · ${h.domain}` : ''} +
+
+ ● {h.status} + {h.health !== 'unknown' && {h.health}} +
+ {h.ip_private && ( +
{h.ip_private}
+ )} +
+ ) +} + +const nodeTypes = { host: HostNode } + +function buildNodes(range: RangeDoc): Node[] { + return range.hosts.map((h, i) => { + const saved = range.layout?.[h.id] + return { + id: h.id, + type: 'host', + position: saved ?? { x: (i % 3) * 220, y: Math.floor(i / 3) * 140 }, + data: h as unknown as Record, + } + }) +} + +export default function RangeView({ sessionId }: { sessionId: string | null }) { + const [range, setRange] = useState(null) + const [nodes, setNodes, onNodesChange] = useNodesState([]) + const [error, setError] = useState(null) + + const load = useCallback(() => { + if (!sessionId) { setRange(null); return } + api.getRange(sessionId) + .then(r => { setRange(r); setNodes(buildNodes(r)); setError(null) }) + .catch(() => setError('range not found')) + }, [sessionId, setNodes]) + + useEffect(() => { load() }, [load]) + + const handleChange = useCallback((changes: NodeChange[]) => { + onNodesChange(changes) + }, [onNodesChange]) + + const persistLayout = useCallback(() => { + if (!sessionId) return + const layout: Record = {} + for (const n of nodes) layout[n.id] = { x: Math.round(n.position.x), y: Math.round(n.position.y) } + api.saveLayout(sessionId, layout).catch(() => {}) + }, [sessionId, nodes]) + + const header = useMemo(() => { + if (!range) return '' + const up = range.hosts.filter(h => h.status === 'running').length + return `${up}/${range.hosts.length} running` + }, [range]) + + if (!sessionId) { + return
No session selected
+ } + + return ( +
+
+ RANGE + {header} +
+
+ {error ? ( +
{error}
+ ) : ( + + + + + )} +
+
+ ) +} + +const headerStyle: React.CSSProperties = { + display: 'flex', alignItems: 'center', justifyContent: 'space-between', + padding: '12px 16px', borderBottom: '1px solid var(--dn-border)', + background: 'var(--dn-black)', flexShrink: 0, +} +const emptyStyle: React.CSSProperties = { + display: 'flex', alignItems: 'center', justifyContent: 'center', + height: '100%', color: 'var(--dn-text-dim)', fontSize: 13, +} diff --git a/webapp/frontend/src/components/TerminalChat.tsx b/webapp/frontend/src/components/TerminalChat.tsx new file mode 100644 index 00000000..87dced26 --- /dev/null +++ b/webapp/frontend/src/components/TerminalChat.tsx @@ -0,0 +1,112 @@ +import { useEffect, useRef, useState } from 'react' +import Markdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import type { ChatEvent } from '../types' +import type { ConnectionStatus } from '../hooks/useWebSocket' + +interface Props { + sessionId: string | null + messages: ChatEvent[] + status: ConnectionStatus + onSend: (content: string) => void +} + +function Badge({ text, color }: { text: string; color: string }) { + return ( + {text} + ) +} + +function toolSummary(ev: ChatEvent): string { + if (ev.tool) { + let a = '' + try { a = JSON.stringify(JSON.parse(ev.args || '{}')) } catch { a = ev.args || '' } + return `${ev.tool} ${a}`.trim() + } + return ev.command || '' +} + +function Message({ ev }: { ev: ChatEvent }) { + switch (ev.kind) { + case 'user_message': + return ( +
+ > + {ev.content} +
+ ) + case 'generation': + return ev.content ? ( +
+ {ev.content} +
+ ) : null + case 'tool_start': + return
{toolSummary(ev)}
+ case 'tool_end': + return ev.result ?
{ev.result}
: null + case 'command_run': + return ev.phase === 'start' + ?
{ev.command}
+ :
exit {String(ev.exit_code)}
+ case 'command_progress': + return
{ev.line}
+ case 'check_run': + return
{ev.error ? `check failed: ${String(ev.error)}` : `range verified — ${ev.hosts_updated ?? 0} host(s) updated`}
+ case 'error': + return
{ev.message}
+ default: + return null + } +} + +export default function TerminalChat({ sessionId, messages, status, onSend }: Props) { + const [input, setInput] = useState('') + const endRef = useRef(null) + + useEffect(() => { endRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) + + const submit = () => { + const t = input.trim() + if (!t || !sessionId || status !== 'connected') return + onSend(t) + setInput('') + } + + return ( +
+
+ AGENT + {status} +
+
+ {!sessionId &&
Create or select a session to begin.
} + {messages.map((ev, i) => )} +
+
+
+ > +