diff --git a/.gitignore b/.gitignore index 555ae97..882a1c6 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,9 @@ test_https.zig test_ws.py workspace/ coverage/ +frontend/dist/ +frontend/node_modules/ +frontend/.astro/ # Test artifacts scripts/ diff --git a/AGENTS.md b/AGENTS.md index ba93231..ac6dde0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,7 @@ Entry: `src/main.zig`. Module root: `src/root.zig` (re-exports all modules as `@ - **Logging:** All via `zero.App.logger` (Info/Err/Debug/Warn). Never use `std.log.*` or `std.debug.print` in app code. - **HTTP client:** `zul.http.Client` crashes on `deinit()` if pool has active connections. Workaround: `postJSONZul()` in `src/http_client.zig` sends `Connection: close` header. Do not remove this. +- **WS upgrade + Vite proxy:** In dev, Vite's `/ws` proxy sometimes closes the connection mid-handshake. The server's `upgradeWebsocket` call returns `error.NotOpenForWriting` and the worker then calls `res.write()` on the now-dead FD. That second write hits Zig stdlib's `posix.sendmsg` which has `.BADF => unreachable` and panics the process. Mitigation: `wsUpgrade` in `src/channels/websocket.zig` catches the upgrade error and returns normally (no 500 response) so httpz's catch path doesn't re-write on a dead FD. Real fix for dev users: set `window.__EVA_WS_HOST__ = 'localhost:8765'` in the browser devtools to bypass the Vite WS proxy. In production (`EVA_SERVE_FRONTEND=true`) there's no Vite proxy and the issue does not occur. - **Telegram HTTPS:** Uses custom `TLSClient` (BearSSL-based), not `zul.http.Client` for polling. Only one `getUpdates` poller at a time — concurrent polls cause 409 conflicts. - **LLM responses:** Non-streaming only (`stream: false`). SSE streaming disabled due to Zig 0.15.2 `std.json.Scanner` buffer sensitivity. - **Allocators:** `GeneralPurposeAllocator` for long-lived structs (SessionManager, MessageBus). `ArenaAllocator` for short-lived parsing tasks only. diff --git a/Makefile b/Makefile index 13b6f12..caa4d14 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ usage: - ps -p $(pgrep -d',' basic) -o %cpu,%mem + ps -p $(pgrep -d',' eva) -o %cpu,%mem trace: strace -c zig build eva > /dev/null diff --git a/docs/eva.png b/docs/eva.png new file mode 100644 index 0000000..14eddab Binary files /dev/null and b/docs/eva.png differ diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..1942fa1 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,152 @@ +# Eva Frontend + +Astro 4 + Tailwind 3 single-page app served as static files. ~144 KB +total (96 KB self-hosted woff2, 28 KB JS, 16 KB CSS, 4 KB HTML). + +## Quick start + +```bash +# One-time: install deps +npm install + +# Dev server (port 4321, proxies /api and /ws to :8765) +npm run dev + +# Production build (outputs to dist/) +npm run build + +# Preview the built bundle locally +npm run preview + +# Type-check (Astro + TS) +npx astro check +``` + +The dev server expects the Eva Zig backend running on `localhost:8765` +(the same port the production binary listens on). Vite proxies both +`/api/*` and `/ws` so the browser sees a single origin. + +> **Note on Vite WS proxy:** Vite's dev WebSocket proxy can drop +> long-lived `/ws` connections every few minutes. If you see your +> assistant's response "vanish" after a minute, set +> `window.__EVA_WS_HOST__ = 'localhost:8765'` in the browser devtools +> console and reload — `ws.ts` will then connect to the backend +> directly, bypassing the proxy. (In production +> `EVA_SERVE_FRONTEND=true` the frontend is served by the backend, so +> `location.host` already points there and this is a no-op.) +> +> The frontend also re-fetches the active chat's messages on every +> successful WS `open` event after the first, so responses that +> arrived while the socket was down still appear in the UI. + +## Environment + +| Eva var | Effect | +|----------------------------|-----------------------------------------------------------| +| `EVA_SERVE_FRONTEND=true` | Make the binary serve `frontend/dist/` from `/` | +| `EVA_FRONTEND_DIR=…` | Override the static dir (default: `frontend/dist`) | +| `EVA_API_KEY=…` | Required for `/api/*` and `/ws`; modal asks for the value | +| `window.__EVA_WS_HOST__` | Browser-only override: WS connects to this host in dev | + +The frontend stores the API key in `sessionStorage` (cleared on tab +close). The WebSocket connection sends it as `?token=…` because the +zero upgrader doesn't currently inspect upgrade headers. + +## Architecture + +``` +src/ + layouts/Base.astro Shell HTML + custom element + pages/index.astro Loading splash (replaced by boot.ts) + lib/ + boot.ts mountEva() — the only entry point + api.ts fetch() wrappers for /api/* + icons.ts Inline SVG icon set + state/ + store.ts Proxy-based reactive store + selectors + ws.ts WebSocket client with exponential backoff, + re-fetches active chat on reconnect + components/ + Shell.ts Top-level 2-pane layout, event delegation + Sidebar.ts Chat list, header, status footer + MainPanel.ts Header + MessageList + Input + modals + MessageList.ts Render messages + streaming delta bubble + Input.ts Auto-resizing textarea + send + SettingsModal.ts Read-only model + 4 per-turn overrides + TracePanel.ts Right drawer showing agent steps + actions.ts data-action / data-form / data-change router + styles/global.css @font-face, design tokens, scrollbar, + focus ring, fade-in / pulse animations +public/ + fonts/*.woff2 Self-hosted variable woff2 (no CDN) + favicon.svg +``` + +### Data flow + +``` + boot.ts + │ + ▼ + ┌────────► store.ts ──────────► Shell.ts re-renders + │ ▲ │ + │ │ ▼ + │ set() │ sidebar + main + │ │ │ + │ ┌────┴────┐ │ + │ │ ws.ts │ ▼ + │ │ api │ actions.ts + │ └─────────┘ │ + │ ▼ + │ user interaction +``` + +- **Single source of truth**: a module-level `state` object in `store.ts`. +- **Mutations**: `set(patch)` or `set(s => partial)`. Schedules a rAF + flush of subscribers. +- **Rendering**: `Shell.ts` subscribes once and re-renders both panes + on every change. Each component is a pure `renderX(state): string` + function — no VDOM, no reconciliation. +- **Events**: delegated through `data-action`, `data-form`, + `data-change` attributes, all routed via `actions.ts`. +- **Streaming**: WS deltas accumulate in `state.pendingDelta[chatId]` + and the `MessageList` renders the tail inline (cursor block + pulse). + The full assistant message replaces the tail when `type: "message"` + arrives. + +### Why no framework? + +The whole UI is ~10 components, <2000 LOC of TS + HTML. A VDOM buys +nothing here and costs bundle size. The trade-off is "re-render the +whole shell on every state change" which is fine at this scale: full +innerHTML rewrite is <5 ms in Chrome. + +If a future feature needs targeted sub-tree patches (e.g. live tool +output with thousands of lines), add a `mount(parent, render)` helper +that diffs the parent's children against a list of keyed nodes. Don't +adopt a framework. + +## Build output + +``` +dist/ + index.html 4 KB - static HTML with root + favicon.svg 1 KB + fonts/ + SpaceGrotesk.woff2 22 KB - variable font (400..700) + DMSans.woff2 36 KB - variable font (400..500) + JetBrainsMono.woff2 31 KB - static + _astro/ + index..css 16 KB - Tailwind purged + hoisted..js 28 KB - all app code (7.8 KB gzipped) +``` + +## Conventions + +- **No emojis** in source, comments, or UI. +- **TypeScript strict mode** is on. `astro check` is the source of truth. +- **No new runtime dependencies** without discussion. The bundle is + intentionally tiny. +- **All async UI work** is awaited; rejections are caught and surfaced + as a transient `role: "system"` message in the active chat. +- **Auth tokens** are session-scoped (sessionStorage), never localStorage. diff --git a/frontend/astro.config.mjs b/frontend/astro.config.mjs new file mode 100644 index 0000000..d694d30 --- /dev/null +++ b/frontend/astro.config.mjs @@ -0,0 +1,33 @@ +import { defineConfig } from 'astro/config'; +import tailwind from '@astrojs/tailwind'; + +// https://astro.build/config +export default defineConfig({ + integrations: [tailwind({ applyBaseStyles: true })], + output: 'static', + server: { + port: 4321, + host: '0.0.0.0', + }, + vite: { + server: { + proxy: { + // Proxy API + WS to the Zig backend during `astro dev`. + // Same shape as the prod single-binary deploy: + // GET /api/* -> eva :8765 + // WS /ws -> eva :8765 + '/api': { + target: 'http://localhost:8765', + changeOrigin: true, + }, + '/ws': { + target: 'ws://localhost:8765', + ws: true, + changeOrigin: true, + }, + }, + }, + }, + // Self-host the Space Grotesk + DM Sans woff2 in /public/fonts/. + // See docs/frontend/fonts.md for the fetch + @font-face wiring. +}); diff --git a/frontend/mock.html b/frontend/mock.html new file mode 100644 index 0000000..b560371 --- /dev/null +++ b/frontend/mock.html @@ -0,0 +1,1885 @@ +```html + + + + + + Eva — AI Agent Chat + + + + + + + + + + + +
+
+
+

+ Settings +

+ +
+
+ +
+ +
+ + + +
+
+ +
+
+ + 0.7 +
+ +
+ PreciseCreative +
+
+ +
+
+ + 2048 +
+ +
+ 2568192 +
+
+ +
+
+ + 0.9 +
+ +
+ 01 +
+
+ +
+ + +
+
+
+ + +
+
+
+ +
+ + + + +
+
+
+ +
+

+ Welcome to Eva +

+
+ + + + + Online — Ready +
+
+
+
+ + +
+
+ +
+
+
+
+
+ +
+

+ Hello, I'm Eva +

+

+ Your AI agent that can reason, search, and take action. Start + a conversation or try a suggestion below. +

+
+
+ +
+ +
+
+ +
+ +
+ + + +
+
+

+ Eva can make mistakes. Consider checking important + information. +

+
+
+
+ + +
+
+
+ +
+ +
+ + + + +``` diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..749fb94 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,7100 @@ +{ + "name": "eva-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "eva-frontend", + "version": "0.1.0", + "dependencies": { + "@astrojs/tailwind": "^5.1.4", + "astro": "^4.16.0", + "nanoid": "^5.0.9", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@astrojs/check": "^0.9.9", + "typescript": "^5.9.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@astrojs/check": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.9.tgz", + "integrity": "sha512-A5UW8uIuErLWEoRQvzgXpO1gTjUFtK8r7nU2Z7GewAMxUb7bPvpk11qaKKgxqXlHJWlAvaaxy+Xg28A6bmQ1Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/language-server": "^2.16.7", + "chokidar": "^4.0.3", + "kleur": "^4.1.5", + "yargs": "^17.7.2" + }, + "bin": { + "astro-check": "bin/astro-check.js" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/@astrojs/check/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@astrojs/check/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@astrojs/compiler": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.1.tgz", + "integrity": "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.4.1.tgz", + "integrity": "sha512-bMf9jFihO8YP940uD70SI/RDzIhUHJAolWVcO1v5PUivxGKvfLZTLTVVxEYzGYyPsA3ivdLNqMnL5VgmQySa+g==", + "license": "MIT" + }, + "node_modules/@astrojs/language-server": { + "version": "2.16.10", + "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.16.10.tgz", + "integrity": "sha512-87VQ/5GSdHlRnUA+hGuerYyIGAj+9RbZmATyuKLEUePinUXhQ5YkRnRrHhOD9sSi5JOErLjrLkHnfZFEvGrV8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.13.1", + "@astrojs/yaml2ts": "^0.2.4", + "@jridgewell/sourcemap-codec": "^1.5.5", + "@volar/kit": "~2.4.28", + "@volar/language-core": "~2.4.28", + "@volar/language-server": "~2.4.28", + "@volar/language-service": "~2.4.28", + "muggle-string": "^0.4.1", + "tinyglobby": "^0.2.16", + "volar-service-css": "0.0.70", + "volar-service-emmet": "0.0.70", + "volar-service-html": "0.0.70", + "volar-service-prettier": "0.0.70", + "volar-service-typescript": "0.0.70", + "volar-service-typescript-twoslash-queries": "0.0.70", + "volar-service-yaml": "0.0.70", + "vscode-html-languageservice": "^5.6.2", + "vscode-uri": "^3.1.0" + }, + "bin": { + "astro-ls": "bin/nodeServer.js" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "prettier-plugin-astro": ">=0.11.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + } + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-5.3.0.tgz", + "integrity": "sha512-r0Ikqr0e6ozPb5bvhup1qdWnSPUvQu6tub4ZLYaKyG50BXZ0ej6FhGz3GpChKpH7kglRFPObJd/bDyf2VM9pkg==", + "license": "MIT", + "dependencies": { + "@astrojs/prism": "3.1.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "import-meta-resolve": "^4.1.0", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.1", + "remark-smartypants": "^3.0.2", + "shiki": "^1.22.0", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/prism": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-3.1.0.tgz", + "integrity": "sha512-Z9IYjuXSArkAUx3N6xj6+Bnvx8OdUSHA8YoOgyepp3+zJmtVYJIl/I18GozdJVW1p5u/CNpl3Km7/gwTJK85cw==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.29.0" + }, + "engines": { + "node": "^18.17.1 || ^20.3.0 || >=21.0.0" + } + }, + "node_modules/@astrojs/tailwind": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-5.1.5.tgz", + "integrity": "sha512-1diguZEau7FZ9vIjzE4BwavGdhD3+JkdS8zmibl1ene+EHgIU5hI0NMgRYG3yea+Niaf7cyMwjeWeLvzq/maxg==", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.20", + "postcss": "^8.5.1", + "postcss-load-config": "^4.0.2" + }, + "peerDependencies": { + "astro": "^3.0.0 || ^4.0.0 || ^5.0.0", + "tailwindcss": "^3.0.24" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.1.0.tgz", + "integrity": "sha512-/ca/+D8MIKEC8/A9cSaPUqQNZm+Es/ZinRv0ZAzvu2ios7POQSsVD+VOj7/hypWNsNM3T7RpfgNq7H2TU1KEHA==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.0.0", + "debug": "^4.3.4", + "dlv": "^1.1.3", + "dset": "^3.1.3", + "is-docker": "^3.0.0", + "is-wsl": "^3.0.0", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "^18.17.1 || ^20.3.0 || >=21.0.0" + } + }, + "node_modules/@astrojs/yaml2ts": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@astrojs/yaml2ts/-/yaml2ts-0.2.4.tgz", + "integrity": "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.8.3" + } + }, + "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==", + "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==", + "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==", + "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/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@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-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "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==", + "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-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "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": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^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==", + "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.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emmetio/abbreviation": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz", + "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-abbreviation": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz", + "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-parser": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@emmetio/css-parser/-/css-parser-0.4.1.tgz", + "integrity": "sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/stream-reader": "^2.2.0", + "@emmetio/stream-reader-utils": "^0.1.0" + } + }, + "node_modules/@emmetio/html-matcher": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emmetio/html-matcher/-/html-matcher-1.3.0.tgz", + "integrity": "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@emmetio/scanner": "^1.0.0" + } + }, + "node_modules/@emmetio/scanner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz", + "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz", + "integrity": "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz", + "integrity": "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz", + "integrity": "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==", + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz", + "integrity": "sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "oniguruma-to-es": "^2.2.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz", + "integrity": "sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/langs": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz", + "integrity": "sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz", + "integrity": "sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.2" + } + }, + "node_modules/@shikijs/types": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz", + "integrity": "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "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/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "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/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, + "node_modules/@volar/kit": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.4.28.tgz", + "integrity": "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-service": "2.4.28", + "@volar/typescript": "2.4.28", + "typesafe-path": "^0.2.2", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/language-server": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-server/-/language-server-2.4.28.tgz", + "integrity": "sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@volar/language-service": "2.4.28", + "@volar/typescript": "2.4.28", + "path-browserify": "^1.0.1", + "request-light": "^0.7.0", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/language-service": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.28.tgz", + "integrity": "sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vscode/emmet-helper": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.11.0.tgz", + "integrity": "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "emmet": "^2.4.3", + "jsonc-parser": "^2.3.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.15.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astro": { + "version": "4.16.19", + "resolved": "https://registry.npmjs.org/astro/-/astro-4.16.19.tgz", + "integrity": "sha512-baeSswPC5ZYvhGDoj25L2FuzKRWMgx105FetOPQVJFMCAp0o08OonYC7AhwsFdhvp7GapqjnC1Fe3lKb2lupYw==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^2.10.3", + "@astrojs/internal-helpers": "0.4.1", + "@astrojs/markdown-remark": "5.3.0", + "@astrojs/telemetry": "3.1.0", + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx": "^7.25.9", + "@babel/types": "^7.26.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.1.3", + "@types/babel__core": "^7.20.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "boxen": "8.0.1", + "ci-info": "^4.1.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^1.0.1", + "cookie": "^0.7.2", + "cssesc": "^3.0.0", + "debug": "^4.3.7", + "deterministic-object-hash": "^2.0.2", + "devalue": "^5.1.1", + "diff": "^5.2.0", + "dlv": "^1.1.3", + "dset": "^3.1.4", + "es-module-lexer": "^1.5.4", + "esbuild": "^0.21.5", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.2", + "flattie": "^1.1.1", + "github-slugger": "^2.0.0", + "gray-matter": "^4.0.3", + "html-escaper": "^3.0.3", + "http-cache-semantics": "^4.1.1", + "js-yaml": "^4.1.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.14", + "magicast": "^0.3.5", + "micromatch": "^4.0.8", + "mrmime": "^2.0.0", + "neotraverse": "^0.6.18", + "ora": "^8.1.1", + "p-limit": "^6.1.0", + "p-queue": "^8.0.1", + "preferred-pm": "^4.0.0", + "prompts": "^2.4.2", + "rehype": "^13.0.2", + "semver": "^7.6.3", + "shiki": "^1.23.1", + "tinyexec": "^0.3.1", + "tsconfck": "^3.1.4", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.3", + "vite": "^5.4.11", + "vitefu": "^1.0.4", + "which-pm": "^3.0.0", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^21.1.1", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.23.5", + "zod-to-ts": "^1.2.0" + }, + "bin": { + "astro": "astro.js" + }, + "engines": { + "node": "^18.17.1 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "optionalDependencies": { + "sharp": "^0.33.3" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz", + "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz", + "integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "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/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "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/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-ancestor-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", + "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", + "license": "ISC" + }, + "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==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/deterministic-object-hash": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deterministic-object-hash/-/deterministic-object-hash-2.0.2.tgz", + "integrity": "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==", + "license": "MIT", + "dependencies": { + "base-64": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/devalue": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", + "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "license": "MIT" + }, + "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/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.368", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", + "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", + "license": "ISC" + }, + "node_modules/emmet": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.11.tgz", + "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "./packages/scanner", + "./packages/abbreviation", + "./packages/css-abbreviation", + "./" + ], + "dependencies": { + "@emmetio/abbreviation": "^2.3.3", + "@emmetio/css-abbreviation": "^2.1.8" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "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/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-yarn-workspace-root2": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz", + "integrity": "sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==", + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2", + "pkg-dir": "^4.2.0" + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.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/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT", + "optional": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", + "dev": true, + "license": "MIT" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/load-yaml-file": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", + "integrity": "sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.5", + "js-yaml": "^3.13.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/load-yaml-file/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/load-yaml-file/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/load-yaml-file/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "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-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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-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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "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/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", + "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oniguruma-to-es": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", + "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^5.1.1", + "regex-recursion": "^5.1.1" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", + "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", + "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "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==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/preferred-pm": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-4.1.1.tgz", + "integrity": "sha512-rU+ZAv1Ur9jAUZtGPebQVQPzdGhNzaEiQ7VL9+cjsAWPHFYOccNXPNiev1CCDSOg/2j7UujM7ojNhpkuILEVNQ==", + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0", + "find-yarn-workspace-root2": "1.2.16", + "which-pm": "^3.0.1" + }, + "engines": { + "node": ">=18.12" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/regex": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", + "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", + "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", + "license": "MIT", + "dependencies": { + "regex": "^5.1.1", + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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/request-light": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz", + "integrity": "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/shiki": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz", + "integrity": "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.29.2", + "@shikijs/engine-javascript": "1.29.2", + "@shikijs/engine-oniguruma": "1.29.2", + "@shikijs/langs": "1.29.2", + "@shikijs/themes": "1.29.2", + "@shikijs/types": "1.29.2", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "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==", + "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/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typesafe-path": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz", + "integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-auto-import-cache": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz", + "integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.8" + } + }, + "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-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^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-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.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-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.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-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.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==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.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": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/volar-service-css": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.70.tgz", + "integrity": "sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-css-languageservice": "^6.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-emmet": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.70.tgz", + "integrity": "sha512-xi5bC4m/VyE3zy/n2CXspKeDZs3qA41tHLTw275/7dNWM/RqE2z3BnDICQybHIVp/6G1iOQj5c1qXMgQC08TNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/css-parser": "^0.4.1", + "@emmetio/html-matcher": "^1.3.0", + "@vscode/emmet-helper": "^2.9.3", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-html": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.70.tgz", + "integrity": "sha512-eR6vCgMdmYAo4n+gcT7DSyBQbwB8S3HZZvSagTf0sxNaD4WppMCFfpqWnkrlGStPKMZvMiejRRVmqsX9dYcTvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-html-languageservice": "^5.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-prettier": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.70.tgz", + "integrity": "sha512-Z6BCFSpGVCd8BPAsZ785Kce1BGlWd5ODqmqZGVuB14MJvrR4+CYz6cDy4F+igmE1gMifqfvMhdgT8Aud4M5ngg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0", + "prettier": "^2.2 || ^3.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + }, + "prettier": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.70.tgz", + "integrity": "sha512-l46Bx4cokkUedTd74ojO5H/zqHZJ8SUuyZ0IB8JN4jfRqUM3bQFBHoOwlZCyZmOeO0A3RQNkMnFclxO4c++gsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-browserify": "^1.0.1", + "semver": "^7.6.2", + "typescript-auto-import-cache": "^0.3.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-nls": "^5.2.0", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript-twoslash-queries": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.70.tgz", + "integrity": "sha512-IdD13Z9N2Bu8EM6CM0fDV1E69olEYGHDU25X51YXmq8Y0CmJ2LNj6gOiBJgpS5JGUqFzECVhMNBW7R0sPdRTMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-yaml": { + "version": "0.0.70", + "resolved": "https://registry.npmjs.org/volar-service-yaml/-/volar-service-yaml-0.0.70.tgz", + "integrity": "sha512-0c8bXDBeoATF9F6iPIlOuYTuZAC4c+yi0siQo920u7eiBJk8oQmUmg9cDUbR4+Gl++bvGP4plj3fErbJuPqdcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8", + "yaml-language-server": "~1.20.0" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/vscode-css-languageservice": { + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.10.tgz", + "integrity": "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-css-languageservice/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-html-languageservice": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.2.tgz", + "integrity": "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-json-languageservice": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.1.8.tgz", + "integrity": "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.2" + }, + "engines": { + "npm": ">=7.0.0" + } + }, + "node_modules/vscode-json-languageservice/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0.tgz", + "integrity": "sha512-+VvMmQPJhtvJ+8O+zu2JKIRiLxXF8NW7krWgyMGeOHrp4Cn23T5hc0v2LknNeopDOB70wghHAds7mKtcZ0I4Sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.0.tgz", + "integrity": "sha512-Zdz+kJ12Iz6tc11xfZyEo501bBATHXrCjmMfnaR3pMnf1CoqZBKIynba3P+/bi9VEdrMbNtAVKYpKhbODvqy+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.0", + "vscode-languageserver-types": "3.18.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz", + "integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver/node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which-pm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-3.0.1.tgz", + "integrity": "sha512-v2JrMq0waAI4ju1xU5x3blsxBBMgdgZve580iYMN5frDaLGjbA24fok7wKCsya8KLVO19Ju4XDc5+zTZCJkQfg==", + "license": "MIT", + "dependencies": { + "load-yaml-file": "^0.2.0" + }, + "engines": { + "node": ">=18.12" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yaml-language-server": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/yaml-language-server/-/yaml-language-server-1.20.0.tgz", + "integrity": "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "prettier": "^3.5.0", + "request-light": "^0.5.7", + "vscode-json-languageservice": "4.1.8", + "vscode-languageserver": "^9.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-uri": "^3.0.2", + "yaml": "2.7.1" + }, + "bin": { + "yaml-language-server": "bin/yaml-language-server" + } + }, + "node_modules/yaml-language-server/node_modules/request-light": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.5.8.tgz", + "integrity": "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml-language-server/node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/zod-to-ts": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/zod-to-ts/-/zod-to-ts-1.2.0.tgz", + "integrity": "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==", + "peerDependencies": { + "typescript": "^4.9.4 || ^5.0.2", + "zod": "^3" + } + }, + "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/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..f056966 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,22 @@ +{ + "name": "eva-frontend", + "type": "module", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "astro dev --port 4321 --host 0.0.0.0", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/tailwind": "^5.1.4", + "astro": "^4.16.0", + "nanoid": "^5.0.9", + "tailwindcss": "^3.4.17" + }, + "devDependencies": { + "@astrojs/check": "^0.9.9", + "typescript": "^5.9.3" + } +} diff --git a/frontend/pending.md b/frontend/pending.md new file mode 100644 index 0000000..a6e6762 --- /dev/null +++ b/frontend/pending.md @@ -0,0 +1,179 @@ +# Pending — Frontend Integration + +Last updated 2026-06-07. Covers everything still open in the +`frontend/` tree and any cross-cutting backend touchpoints the +frontend depends on but does not yet drive. + +## P1 — Ship blockers + +### 1. CI does not build the frontend +`.github/workflows/ci.yml` runs `zig build test` + kcov but has no +`npm ci && npm run build` step. Add a job (matrix-friendly; can share +the same imng/zero-kcov:0.1 image — Node 22 is included) that: +1. `cd frontend && npm ci` +2. `npx astro check` (must report 0 errors / 0 warnings) +3. `npm run build` (asserts `dist/index.html` exists) + +Without this, frontend regressions are invisible to CI. + +### 2. Root `README.md` has no Frontend section +The top-level README is Zig-centric. Add a `## Frontend` section +pointing at `frontend/README.md`, listing `npm run dev` / +`npm run build` / `npx astro check` as the dev workflow, and calling +out `EVA_SERVE_FRONTEND=true` + `EVA_FRONTEND_DIR` for the +single-binary deploy path. + +### 3. `run.sh` does not start Vite alongside the backend +`run.sh` only starts the Zig binary. For a true dev loop we need: +- export `EVA_API_KEY=...` (and any LLM key the user has) +- `zig build run &` in background +- `cd frontend && npm run dev` in foreground (Vite proxies /api and /ws + to :8765) +- clean shutdown on Ctrl-C (kill both, not just the foreground) +The file is already in `.gitignore` per AGENTS.md because of a +hardcoded Telegram token; the rewrite should keep the token source +external (`$EVA_TELEGRAM_TOKEN`). + +## P2 — Functional gaps + +### 4. File attachment is a no-op +`frontend/src/components/actions.ts:46` still does +`pushToast('File attachment coming soon')`. The Input.ts paperclip +button is rendered (`eva-attach-btn`) but the action does nothing +real. To finish: +- Backend: a new `/api/upload` route that accepts multipart, writes + under `workspace/attachments//`, returns + `{ id, name, size, mime, url }`. 16 MiB cap, MIME allowlist + (text/*, image/png|jpeg|webp|gif, application/pdf). Reuse the + workspace-sandbox check from `tools/filesystem.zig`. +- Backend: an `attach_file` tool that the agent loop can call to + reference an attachment id from a tool result. +- Frontend: extend `actions.ts` `case 'attach'` to open a hidden + ``; on change, POST to `/api/upload`, + push a `role: "system"` placeholder into the chat with the upload + ids, and surface the chip via `eva-attached-context`. +- `remove-context` action (`actions.ts:47`) already hides the chip + element but does not delete the upload or undo the placeholder + message; both should be wired. + +### 5. TracePanel still shows MOCK_STEPS +`frontend/src/components/TracePanel.ts:16` defines +`MOCK_STEPS: MockStep[]` rendered until the first real `trace` event +for the active chat arrives. Two acceptable fixes (pick one): +- Empty state: render a "Waiting for the next step..." line + 3 + animated dots; drop MOCK_STEPS entirely. +- Show on first turn only: keep the mock for the first user message + in a chat (so the panel isn't empty on a brand-new session) and + swap to real events on the second turn onward. +The mock is currently visible every time the user opens the panel for +a fresh chat, which is the most likely time they look at it — a +strong signal that "Understanding → Retrieving → Planning → +Generating → Verifying" is something Eva actually does, which it +isn't. Either fix is honest; the empty state is more truthful. + +### 6. WS has no heartbeat / dead-connection detection +The reconnect logic (`frontend/src/state/ws.ts`) is reactive — it +notices a dead socket only when `close` / `error` fires. A +half-open connection (e.g. NAT timeout, server crash without FIN) +will hang forever. Add: +- A `ping` WS message type on the client side, sent every 25 s + via `setInterval` (cleared on close). +- Server side: a `pong` reply in `channels/websocket.zig` + `clientMessage` when `type == "ping"`. Constant work, no + allocator churn. +- Client side: if no message (pong or otherwise) arrives within + 60 s of the last ping, force-close the socket so the existing + exponential backoff kicks in. + +This pairs with the reconnect re-fetch added in commit (the re-fetch +already exists, so a forced close is a no-op for the user beyond a +brief "Reconnecting..." flash). + +## P3 — Validate end-to-end + +These are already wired in the code but have not been exercised +against the running app. The user reported WS drops in dev; until +that's fixed, several of these can't be observed. + +### 7. Mobile sidebar toggle +`MainPanel.ts:24` hamburger + `Shell.ts:36` overlay + `Sidebar.ts:35` +close button all hit `data-action="toggle-sidebar"`, which flips +`store.sidebarOpen`. The CSS that animates the overlay +(`eva-sidebar-overlay.on`) and the `translate-x` on the sidebar +needs to be confirmed against an actual narrow viewport (DevTools +responsive mode). If the body scroll lock is missing, add +`document.body.style.overflow = 'hidden'` while open. + +### 8. Trace panel toggle button +`MainPanel.ts:56` `data-action="toggle-trace"` toggles `traceOpen`, +and `TracePanel.ts` reads it. Confirm the panel actually appears on +the right at `lg+` (currently `hidden lg:flex`) and that toggling +works on first click after page load (the panel defaults to closed, +so a click on the trace button is the only first-time trigger). + +### 9. Reconnect re-fetch in practice +`ws.ts:83-97` calls `api.getMessages(activeId)` on every WS `open` +after the first. To validate: disconnect wifi for 5 s, reconnect, +send a message — the assistant response should appear in the UI +even if the WS drop happened mid-stream. The `__EVA_WS_HOST__` +bypass exists specifically so this can be tested deterministically +(set it in devtools console, reload, kill the backend for 3 s, +restart, observe). + +### 10. `window.__EVA_WS_HOST__` dev override +`ws.ts:61-67` checks for this global; setting +`window.__EVA_WS_HOST__ = 'localhost:8765'` before page load makes +the WS connect direct to the backend, skipping Vite's WS proxy. Not +documented anywhere yet (added it to `frontend/README.md` already, +but no end-to-end test confirms it actually fixes the drops the +user observed). + +## Deferred — v2 / out of scope for v1 + +These are tracked so they aren't forgotten, but the spec explicitly +puts them outside v1. + +- **Dark mode toggle.** Theme is light-only. `tailwind.config.mjs` + has no `dark:` variants, no `data-theme` attribute, no + `prefers-color-scheme` media query. +- **Mobile trace drawer.** The trace panel is `hidden lg:flex`; + mobile users simply don't see trace events. A slide-up sheet is + the planned fix. +- **Desktop sidebar collapse.** The spec calls for a + `eva-sidebar-collapsed` body class that narrows the sidebar to + icon-only width. The store already has `sidebarOpen` (mobile + only); a second `sidebarCollapsed` (desktop, persisted) would be + needed. +- **Real per-model selection.** The settings modal's 3 radio cards + are decorative. Backend has exactly one `EVA_LLM_MODEL` from + env. Wiring the radio to actually call a different provider/model + requires a multi-model provider registry in `providers/`. +- **Frontend test runner.** No `vitest` / `playwright` in + `package.json`. The store and `actions.ts` are the most + testable units; the WS reconnect logic in `ws.ts` is the highest + leverage target. + +## Production hardening + +- **Security headers in `wsStatic`.** `channels/websocket.zig:511` + serves `dist/*` with no `Content-Security-Policy`, + `X-Content-Type-Options: nosniff`, `Referrer-Policy`, or + `Strict-Transport-Security`. CSP needs the SHA-256 hashes of + the bundled `hoisted.*.js` and `index.*.css` to be + effective — generate them at build time or use `'self'` + + nonces. At minimum add `X-Content-Type-Options: nosniff` and + `Referrer-Policy: strict-origin-when-cross-origin`. +- **`EVA_FRONTEND_DIR` is not in the frontend README.** It is in + the project root `AGENTS.md` but `frontend/README.md` only lists + `EVA_SERVE_FRONTEND` and `EVA_API_KEY`. Add a row. +- **Wrong API key flow is untested.** The `authRequired` modal + shows on 401, but no automated check covers "user enters wrong + key, server rejects WS upgrade, modal re-appears". A 1-page + Playwright test would lock the contract. +- **No `Connection: keep-alive` policy documented.** Vite proxy + + `EVA_SERVE_FRONTEND` both default to whatever the underlying + stack provides. If a reverse proxy (nginx, caddy) is placed in + front of the single-binary deploy, its idle timeout will become + the effective WS drop interval. Document a recommended + `proxy_read_timeout 3600s;` (nginx) or `timeouts { none }` + (caddy) for any production proxy. diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..d3dd683 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/public/fonts/DMSans.woff2 b/frontend/public/fonts/DMSans.woff2 new file mode 100644 index 0000000..01383d7 Binary files /dev/null and b/frontend/public/fonts/DMSans.woff2 differ diff --git a/frontend/public/fonts/JetBrainsMono.woff2 b/frontend/public/fonts/JetBrainsMono.woff2 new file mode 100644 index 0000000..4d09cda Binary files /dev/null and b/frontend/public/fonts/JetBrainsMono.woff2 differ diff --git a/frontend/public/fonts/SpaceGrotesk.woff2 b/frontend/public/fonts/SpaceGrotesk.woff2 new file mode 100644 index 0000000..0f3474e Binary files /dev/null and b/frontend/public/fonts/SpaceGrotesk.woff2 differ diff --git a/frontend/src/components/Input.ts b/frontend/src/components/Input.ts new file mode 100644 index 0000000..c97de25 --- /dev/null +++ b/frontend/src/components/Input.ts @@ -0,0 +1,63 @@ +// Input box: status bar (above) + textarea + attach/send row. +// Matches mock.html lines 961-1031. The textarea is always enabled +// so the user can type a message and we'll create a new chat on +// submit if none exists. The send button is disabled only while +// a stream is in flight (status streaming/tool). + +import type { Store } from '../state/store.ts'; +import { icons } from '../lib/icons.ts'; + +const HARD_LIMIT = 4000; + +export function renderInput(s: Store): string { + const showStatus = s.status === 'streaming' || s.status === 'tool'; + const sendingDisabled = showStatus; + return ` + ${showStatus ? ` +
+
+ ${icons.gear} +
+ ${s.status === 'tool' ? 'Running tool…' : 'Agent is thinking…'} + +
+ ` : ''} +
+ +
+ + + +
+
+ `; +} diff --git a/frontend/src/components/MainPanel.ts b/frontend/src/components/MainPanel.ts new file mode 100644 index 0000000..de66f9f --- /dev/null +++ b/frontend/src/components/MainPanel.ts @@ -0,0 +1,94 @@ +// Main panel: header (title + online status + model button + trace +// toggle), message list, input box, settings modal (if open). +// Trace panel is rendered by TracePanel.ts and is persistent on +// desktop (lg+); see Shell.ts for the layout glue. + +import type { Store } from '../state/store.ts'; +import { renderMessageList } from './MessageList.ts'; +import { renderInput } from './Input.ts'; +import { renderSettingsModal } from './SettingsModal.ts'; +import { icons } from '../lib/icons.ts'; + +export function renderMain(s: Store): string { + const cfg = s.config; + const title = s.activeId + ? s.sessions.find(c => c.id === s.activeId)?.title ?? 'New chat' + : 'Welcome to Eva'; + const statusText = statusTextFor(s); + const dotColor = s.connection === 'open' ? 'bg-teal' : s.connection === 'connecting' ? 'bg-yellow-400' : 'bg-red-400'; + const pulseClass = s.connection === 'open' ? 'eva-pulse' : ''; + const modelLabel = cfg?.model || s.settingsModel || 'Eva-4 Turbo'; + return ` +
+
+ +
+

+ ${escapeHtml(title)} +

+
+ + + + + ${statusText} + ${s.connection === 'closed' || s.connection === 'error' ? ` + + ` : ''} +
+
+
+
+ + +
+
+ +
+ ${renderMessageList(s)} +
+ +
+
+ ${renderInput(s)} +

+ Eva can make mistakes. Consider checking important information. +

+
+
+ + ${s.settingsOpen ? renderSettingsModal(s) : ''} + `; +} + +function statusTextFor(s: Store): string { + if (s.status === 'streaming') return 'Eva is responding…'; + if (s.status === 'tool') return 'Running tool…'; + if (s.connection === 'open') return 'Online — Ready'; + if (s.connection === 'connecting') return s.wsRetries > 0 ? `Reconnecting (${s.wsRetries})` : 'Connecting…'; + if (s.connection === 'error') return 'Connection error'; + return 'Disconnected'; +} + +function escapeHtml(s: string): string { + return s.replace(/[&<>"']/g, ch => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + }[ch]!)); +} diff --git a/frontend/src/components/MessageList.ts b/frontend/src/components/MessageList.ts new file mode 100644 index 0000000..27cab07 --- /dev/null +++ b/frontend/src/components/MessageList.ts @@ -0,0 +1,143 @@ +// Message list. Renders the welcome screen (with 3 suggestion cards) +// when no chat is active or the active chat is empty, otherwise +// renders the message history plus a streaming tail. +// +// User bubbles are pink, AI bubbles are white cards. Streaming uses +// a 3-dot typing animation when there's no delta yet; once deltas +// arrive the bubble grows. Each AI message has a copy button. + +import type { Store, Message, Suggestion } from '../state/store.ts'; +import { sel } from '../state/store.ts'; +import { icons } from '../lib/icons.ts'; + +const MAX_RENDER = 200; + +export function renderMessageList(s: Store): string { + if (!s.activeId) return welcomeScreen(s); + const msgs = sel.activeMessages(s); + if (msgs.length === 0) return welcomeScreen(s); + + const slice = msgs.length > MAX_RENDER ? msgs.slice(-MAX_RENDER) : msgs; + const tail = s.pendingDelta[s.activeId] ?? ''; + const items: string[] = slice.map((m, i) => renderMessage(m, i)); + + if (s.status === 'streaming') { + items.push(tail ? renderStreamingBubble(tail) : renderTypingDots()); + } else if (s.status === 'tool') { + items.push(renderToolRunning()); + } + + return `
${items.join('')}
`; +} + +function welcomeScreen(s: Store): string { + return ` +
+
+ ${icons.robot} +
+

Hello, I'm Eva

+

+ Your AI agent that can reason, search, and take action. + Start a conversation below. +

+
+ `; +} + +function suggestionCard(s: Suggestion): string { + const colorClass = s.iconColor === 'pink' ? 'text-pink' : s.iconColor === 'teal' ? 'text-teal' : 'text-purple'; + // Map kebab-case store keys → camelCase icon names. + const key = s.icon.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) as keyof typeof icons; + return ` + + `; +} + +function renderMessage(m: Message, idx: number): string { + if (m.role === 'user') { + return ` +
+
+ ${escapeHtml(m.content)} +
+
+ `; + } + if (m.role === 'system') { + return ` +
+
+ ${escapeHtml(m.content)} +
+
+ `; + } + // tool messages are intermediate artifacts — skip + if (m.role === 'tool') return ''; + // assistant + return ` +
+
+
+ ${escapeHtml(m.content)} +
+ +
+
+ `; +} + +function renderStreamingBubble(tail: string): string { + return ` +
+
+ ${escapeHtml(tail)} +
+
+ `; +} + +function renderTypingDots(): string { + return ` +
+
+ + + +
+
+ `; +} + +function renderToolRunning(): string { + return ` +
+
+ ${icons.gear} + Running tool… +
+
+ `; +} + +function escapeHtml(s: string): string { + return s.replace(/[&<>"']/g, ch => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + }[ch]!)); +} +function escapeAttr(s: string): string { + return s.replace(/"/g, '"'); +} diff --git a/frontend/src/components/SettingsModal.ts b/frontend/src/components/SettingsModal.ts new file mode 100644 index 0000000..1733596 --- /dev/null +++ b/frontend/src/components/SettingsModal.ts @@ -0,0 +1,152 @@ +// Settings modal. Matches mock.html lines 549-770: +// - 3 model radio cards (Turbo / Pro / 3.5 Fast) with icons +// - 3 sliders with live value labels (temp / maxTokens / topP) +// - System prompt textarea +// - Reset (left) + Apply (right, pink) buttons +// - Backdrop overlay + scale-in entry animation +// +// The model radio cards are decorative when the backend has only one +// model — selecting a non-configured one still updates the header +// label (visual only). The actual generation uses server-side config. + +import type { Store } from '../state/store.ts'; +import { icons } from '../lib/icons.ts'; +import { getOverrides } from './actions.ts'; + +const MODELS = [ + { id: 'Eva-4 Turbo', desc: 'Fast & general', icon: icons.bolt, color: 'text-pink' }, + { id: 'Eva-4 Pro', desc: 'Deep reasoning', icon: icons.star, color: 'text-purple' }, + { id: 'Eva-3.5 Fast', desc: 'Lightweight', icon: icons.gaugeHigh, color: 'text-teal' }, +]; + +export function renderSettingsModal(s: Store): string { + const o = getOverrides(); + const temp = s.settingsTemp ?? o.temperature ?? 0.7; + const tokens = s.settingsMaxTokens ?? o.max_tokens ?? 2048; + const topP = s.settingsTopP ?? o.top_p ?? 0.9; + const sysPrompt = s.settingsSysPrompt || o.system_prompt || "You are Eva, a helpful AI agent that can reason, search, and take action. Be concise and precise."; + return ` +
+
+
+

Settings

+ +
+
+ +
+ +
+ ${MODELS.map(m => modelRadio(m, s.settingsModel || s.config?.model || 'Eva-4 Turbo')).join('')} +
+
+ +
+
+ + ${temp.toFixed(1)} +
+ +
+ Precise + Creative +
+
+ +
+
+ + ${tokens} +
+ +
+ 256 + 8192 +
+
+ +
+
+ + ${topP.toFixed(2)} +
+ +
+ 0 + 1 +
+
+ +
+ + +
+ ${s.authRequired ? ` +
+ + +
+ ` : ''} +
+
+ + +
+
+
+ `; +} + +function modelRadio(m: typeof MODELS[number], selected: string): string { + const checked = m.id === selected; + return ` + + `; +} + +function escapeHtml(s: string): string { + return s.replace(/[&<>"']/g, ch => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + }[ch]!)); +} diff --git a/frontend/src/components/Shell.ts b/frontend/src/components/Shell.ts new file mode 100644 index 0000000..14fbc9b --- /dev/null +++ b/frontend/src/components/Shell.ts @@ -0,0 +1,239 @@ +// Top-level Shell. Three-pane layout: sidebar (left) | main (center) +// | agent trace (right, persistent on desktop). Sub-components are +// rendered via innerHTML on store changes; this keeps the framework- +// free architecture and avoids re-implementing a VDOM. +// +// Critical detail: the message-input textarea preserves its value + +// height across re-renders so typing isn't reset by every store +// mutation (toasts, message arrival, etc). + +import { subscribe, set, type Store } from '../state/store.ts'; +import { renderSidebar } from './Sidebar.ts'; +import { renderMain } from './MainPanel.ts'; +import { renderTracePanel } from './TracePanel.ts'; +import { renderToastStack } from './Toasts.ts'; +import { + handleClick, handleSubmit, handleChange, handleInput, handleSlider, handleModelRadio, + onDragStart, onDragEnd, onDragOver, onDrop, +} from './actions.ts'; + +export function renderShell(shell: HTMLElement, store: Store): void { + // Preserve ephemeral UI state across re-renders. The textarea + // value is the most important; if we don't keep it, every store + // change (toast, message arrival, status flip) wipes what the + // user has typed so far. + const draft = readDraft(shell); + const focus = readFocus(shell); + const scroll = readScroll(shell); + + shell.innerHTML = ` +
+ ${renderSidebar(store)} +
+ ${renderMain(store)} +
+ ${renderTracePanel(store)} +
+
+ ${renderToastStack(store)} + `; + + restoreDraft(shell, draft, focus); + restoreScroll(shell, scroll); + wireEvents(shell); +} + +function readDraft(shell: HTMLElement): { value: string; height: string } | null { + const ta = shell.querySelector('textarea[name="content"]'); + if (!ta) return null; + return { value: ta.value, height: ta.style.height }; +} + +function readFocus(_shell: HTMLElement): { tag: string; id: string } | null { + const a = document.activeElement as HTMLElement | null; + if (!a) return null; + const sel = `textarea[name="content"], input[data-search], textarea[data-sys-prompt]`; + if (!a.matches(sel)) return null; + const offset = a.tagName === 'TEXTAREA' ? (a as HTMLTextAreaElement).selectionStart : 0; + return { tag: a.tagName, id: (a.dataset.id ?? '') + ':' + offset }; +} + +function readScroll(shell: HTMLElement): { top: number; height: number; nearBottom: boolean } | null { + const el = shell.querySelector('#eva-messages'); + if (!el) return null; + const threshold = 120; + return { + top: el.scrollTop, + height: el.scrollHeight, + nearBottom: el.scrollHeight - el.scrollTop - el.clientHeight < threshold, + }; +} + +function restoreScroll(shell: HTMLElement, scroll: { top: number; height: number; nearBottom: boolean } | null): void { + if (!scroll) return; + const el = shell.querySelector('#eva-messages'); + if (!el) return; + if (scroll.nearBottom) { + el.scrollTop = el.scrollHeight; + } else { + // Content grew: keep the same relative position from top. + const delta = el.scrollHeight - scroll.height; + el.scrollTop = scroll.top + delta; + } +} + +function restoreDraft(shell: HTMLElement, draft: { value: string; height: string } | null, _focus: { tag: string; id: string } | null): void { + if (!draft) return; + const ta = shell.querySelector('textarea[name="content"]'); + if (!ta) return; + // Only restore if the value isn't already set by the render path + // (e.g. we just submitted and cleared it). Comparing to '' is a + // good proxy: after submit, value is ''. If a re-render happens + // while the user is typing, the new textarea will also be '' and + // we restore their draft. + if (ta.value === '' && draft.value !== '') { + ta.value = draft.value; + ta.style.height = draft.height || 'auto'; + if (draft.height === '') ta.style.height = 'auto'; + } +} + +function wireEvents(shell: HTMLElement): void { + // wireEvents is called from renderShell(), which fires on every + // store change. shell.innerHTML = ... replaces the shell's children + // but NOT the shell element itself, so addEventListener on the + // shell would accumulate: after N renders, N copies of onSubmit are + // attached. A single Enter keypress then fires sendUserMessage N + // times, the backend processes N copies, the user sees the response + // N times. removeEventListener with the same function reference + // detaches the previous copy, so calling this from renderShell is + // safe — listeners are stable module-level functions. + shell.removeEventListener('click', onClick); + shell.addEventListener('click', onClick); + shell.removeEventListener('submit', onSubmit); + shell.addEventListener('submit', onSubmit); + shell.removeEventListener('input', onInput); + shell.addEventListener('input', onInput); + shell.removeEventListener('change', onChange); + shell.addEventListener('change', onChange); + shell.removeEventListener('keydown', onKeydown); + shell.addEventListener('keydown', onKeydown); + shell.removeEventListener('dragstart', onDragStart as unknown as EventListener); + shell.addEventListener('dragstart', onDragStart as unknown as EventListener); + shell.removeEventListener('dragend', onDragEnd as unknown as EventListener); + shell.addEventListener('dragend', onDragEnd as unknown as EventListener); + shell.removeEventListener('dragover', onDragOver as unknown as EventListener); + shell.addEventListener('dragover', onDragOver as unknown as EventListener); + shell.removeEventListener('drop', onDrop as unknown as EventListener); + shell.addEventListener('drop', onDrop as unknown as EventListener); +} + +function onClick(e: MouseEvent): void { + const t = (e.target as HTMLElement).closest('[data-action]'); + if (!t) return; + const action = t.dataset.action!; + // For overlay-based close, only act when the click landed on the + // backdrop element itself (id="settingsOverlay"), not on a child + // inside the modal. This is necessary because the X close button + // also has data-action="close-settings", and its SVG icon children + // would make e.target !== t (click on / vs
+
+
+
+ ${icons.robot} +
+
+

Eva

+ AI Agent +
+
+ +
+ +
+ +
+
+ ${icons.magnifyingGlass} + +
+
+ +
+ ${visible.length === 0 ? ` +
+ ${s.searchQuery ? 'No matches.' : s.showingArchived ? 'No archived chats.' : 'No conversations yet.'} +
+ ` : visible.map(c => chatItem(c, s.activeId === c.id, s.showingArchived)).join('')} +
+ +
+ +
+ +
+ +
+
U
+
+

User

+

user@eva.ai

+
+
+
+ + `; +} + +function chatItem(c: Chat, isActive: boolean, showingArchived: boolean): string { + const title = c.title ?? new Date(c.created_at).toLocaleDateString(); + return ` +
+ + ${escapeHtml(title)} + + + ${!showingArchived ? ` + + ` : ` + + `} + +
+ `; +} + +function escapeHtml(s: string): string { + return s.replace(/[&<>"']/g, ch => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + }[ch]!)); +} diff --git a/frontend/src/components/Toasts.ts b/frontend/src/components/Toasts.ts new file mode 100644 index 0000000..2874cdb --- /dev/null +++ b/frontend/src/components/Toasts.ts @@ -0,0 +1,22 @@ +// Bottom-center toast stack. Slide-up notifications for transient +// events (settings applied, chat deleted, etc). Auto-dismissed by +// pushToast() in store.ts. + +import type { Store } from '../state/store.ts'; + +export function renderToastStack(s: Store): string { + if (s.toasts.length === 0) return ''; + return ` +
+ ${s.toasts.map(t => ` +
${escapeHtml(t.text)}
+ `).join('')} +
+ `; +} + +function escapeHtml(s: string): string { + return s.replace(/[&<>"']/g, ch => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + }[ch]!)); +} diff --git a/frontend/src/components/TracePanel.ts b/frontend/src/components/TracePanel.ts new file mode 100644 index 0000000..7ebc713 --- /dev/null +++ b/frontend/src/components/TracePanel.ts @@ -0,0 +1,83 @@ +// Agent trace panel. Persistent right-side drawer (mock.html lines +// 1034-1079). Hidden when state.traceOpen is false. Shows real +// trace events when the active chat has any, otherwise the mock +// 5-step template (so the panel never feels empty before the user +// has sent a message). Each step has a vertical connector line +// between it and the next, drawn with a CSS gradient. +// +// Step icons: brain (Understanding), magnifyingGlass (Retrieving), +// sitemap (Planning), penFancy (Generating), shieldHalved (Verifying). +// Each step row is color-coded via the icon color. + +import type { Store, TraceEntry } from '../state/store.ts'; +import { icons } from '../lib/icons.ts'; + +type MockStep = { icon: keyof typeof icons; label: string; detail: string; time: string; color: string }; +const MOCK_STEPS: MockStep[] = [ + { icon: 'brain', label: 'Understanding', detail: 'Parsing user intent and context', time: '0.2s', color: 'text-pink' }, + { icon: 'magnifyingGlass', label: 'Retrieving', detail: 'Searching knowledge base for information', time: '1.1s', color: 'text-purple' }, + { icon: 'sitemap', label: 'Planning', detail: 'Structuring response with 4 key sections', time: '0.4s', color: 'text-teal' }, + { icon: 'penFancy', label: 'Generating', detail: 'Composing detailed response with examples', time: '2.3s', color: 'text-pink' }, + { icon: 'shieldHalved', label: 'Verifying', detail: 'Checking factual accuracy and completeness', time: '0.6s', color: 'text-teal' }, +]; + +export function renderTracePanel(s: Store): string { + if (!s.traceOpen) return ''; + const entries = s.activeId ? s.trace[s.activeId] ?? [] : []; + const usingMock = entries.length === 0; + + return ` + + `; +} + +function mockTraceList(): string { + return ` +
+
+ ${icons.satelliteDish} +
+

No active trace

+

Send a message to see the agent's reasoning steps

+
+ `; +} + +function realTraceList(entries: TraceEntry[]): string { + return ` +
    + ${entries.map((e) => ` +
  1. + + ${icons.gear} + +
    +

    ${escapeHtml(e.step)}

    + ${e.elapsed_ms}ms +
    +

    ${escapeHtml(e.label)}

    + ${e.detail ? `
    ${escapeHtml(e.detail)}
    ` : ''} +
  2. + `).join('')} +
+ `; +} + +function escapeHtml(s: string): string { + return s.replace(/[&<>"']/g, ch => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + }[ch]!)); +} diff --git a/frontend/src/components/actions.ts b/frontend/src/components/actions.ts new file mode 100644 index 0000000..e678fb0 --- /dev/null +++ b/frontend/src/components/actions.ts @@ -0,0 +1,407 @@ +// Data-action dispatcher + form/change handlers. Every interactive +// element in the UI is tagged with data-action="..." (clicks), +// data-form="..." (submits), or data-change="..." (changes) plus +// a few specialized markers (data-search, data-slider, data-model-radio, +// data-sys-prompt, data-rename). This single module routes them to +// the right handler. Statically imported by Shell.ts. + +import { set, getStore, pushToast, type Store, type Chat } from '../state/store.ts'; +import { api } from '../lib/api.ts'; +import { sendMessage, sendCancel, closeWs, connectWs } from '../state/ws.ts'; +import { icons } from '../lib/icons.ts'; + +// Per-turn settings. Picked up by the next sendMessage() call; we +// do NOT mutate the server-side config (which is read-only). The +// model selected in the radio cards is also stored here so the +// header label updates. +const overrides = { + model: 'Eva-4 Turbo' as string, + temperature: undefined as number | undefined, + top_p: undefined as number | undefined, + max_tokens: undefined as number | undefined, + system_prompt: '' as string, +}; + +export function getOverrides() { + return { ...overrides }; +} + +export async function handleClick(action: string, id: string | undefined, el: HTMLElement, _store: Store): Promise { + switch (action) { + case 'new-chat': await createNewChat(); break; + case 'open-chat': if (id) await openChat(id); break; + case 'delete-chat': if (id) await deleteChat(id); break; + case 'archive-chat': if (id) await archiveChat(id, true); break; + case 'unarchive-chat': if (id) await archiveChat(id, false); break; + case 'toggle-archive-view': set(s => ({ ...s, showingArchived: !s.showingArchived })); break; + case 'toggle-sidebar': set(s => ({ ...s, sidebarOpen: !s.sidebarOpen })); break; + case 'open-settings': set({ settingsOpen: true }); break; + case 'close-settings': set({ settingsOpen: false }); break; + case 'open-trace': set({ traceOpen: true }); break; + case 'close-trace': set({ traceOpen: false }); break; + case 'toggle-trace': set(s => ({ traceOpen: !s.traceOpen })); break; + case 'apply-settings': applySettings(); break; + case 'reset-settings': resetSettings(); break; + case 'use-suggestion': if (el.dataset.text) useSuggestion(el.dataset.text); break; + case 'attach': pushToast('File attachment coming soon'); break; + case 'remove-context': document.getElementById('attachedContext')?.classList.add('hidden'); break; + case 'copy-message': if (el.dataset.idx) await copyMessage(Number(el.dataset.idx), el); break; + case 'cancel-stream': cancelStream(); break; + case 'toggle-favorite': if (id) await toggleFavorite(id); break; + case 'start-rename': if (id) startRename(id, el); break; + case 'start-drag': /* handled by dragstart in Shell */ break; + case 'reconnect-ws': reconnectWs(); break; + } +} + +export async function handleSubmit(which: string, form: HTMLFormElement, _store: Store): Promise { + switch (which) { + case 'send-message': { + const ta = form.querySelector('textarea[name="content"]')!; + const content = ta.value.trim(); + if (!content) return; + ta.value = ''; + ta.style.height = 'auto'; + await sendUserMessage(content); + break; + } + } +} + +export async function handleChange(which: string, el: HTMLInputElement | HTMLSelectElement, _store: Store): Promise { + switch (which) { + case 'api-key': { + set({ apiKey: el.value }); + closeWs(); + connectWs(); + break; + } + } +} + +// Live input events (search field, sliders, model radio, sys prompt). +// These need to update state immediately as the user types/drags. +export function handleInput(which: string, el: HTMLInputElement | HTMLTextAreaElement, _store: Store): void { + switch (which) { + case 'search': set({ searchQuery: el.value }); break; + case 'sys-prompt': set({ settingsSysPrompt: el.value }); break; + } +} + +export function handleSlider(name: 'temp' | 'tokens' | 'topP', value: number, _store: Store): void { + switch (name) { + case 'temp': set({ settingsTemp: value }); break; + case 'tokens': set({ settingsMaxTokens: value }); break; + case 'topP': set({ settingsTopP: value }); break; + } +} + +export function handleModelRadio(model: string, _store: Store): void { + set({ settingsModel: model }); +} + +function applySettings(): void { + const s = getStore(); + overrides.model = s.settingsModel; + overrides.temperature = s.settingsTemp ?? undefined; + overrides.top_p = s.settingsTopP ?? undefined; + overrides.max_tokens = s.settingsMaxTokens ?? undefined; + overrides.system_prompt = s.settingsSysPrompt; + set({ settingsOpen: false }); + pushToast('Settings applied', 'success'); +} + +function resetSettings(): void { + set({ + settingsModel: '', + settingsTemp: 0.7, + settingsMaxTokens: 2048, + settingsTopP: 0.9, + settingsSysPrompt: 'You are Eva, a helpful AI agent that can reason, search, and take action. Be concise and precise.', + }); + pushToast('Settings reset to defaults', 'info'); +} + +function useSuggestion(text: string): void { + const ta = document.querySelector('textarea[name="content"]'); + if (ta) { + ta.value = text; + ta.style.height = 'auto'; + ta.style.height = ta.scrollHeight + 'px'; + ta.focus(); + } +} + +async function copyMessage(idx: number, btn: HTMLElement): Promise { + const s = getStore(); + if (!s.activeId) return; + const msgs = s.messages[s.activeId] ?? []; + const m = msgs[idx]; + if (!m) return; + try { + await navigator.clipboard.writeText(m.content); + const original = btn.innerHTML; + btn.innerHTML = `${icons.check}`; + setTimeout(() => { btn.innerHTML = original; }, 1500); + } catch (e) { + console.error('copy failed', e); + } +} + +async function createNewChat(): Promise { + const id = crypto.randomUUID(); + const now = Date.now(); + const chat: Chat = { + id, + title: 'New chat', + created_at: now, + updated_at: now, + archived: false, + favorite: false, + sort_order: now, + message_count: 0, + }; + set(s => ({ ...s, sessions: [chat, ...s.sessions], activeId: id, messages: { ...s.messages, [id]: [] } })); +} + +async function openChat(id: string): Promise { + set(s => ({ ...s, activeId: id, sidebarOpen: false })); + if (!getStore().messages[id]) { + try { + const messages = await api.getMessages(id); + set(s => ({ ...s, messages: { ...s.messages, [id]: messages } })); + } catch (e) { + console.error('load messages failed', e); + } + } +} + +async function deleteChat(id: string): Promise { + try { + await api.deleteSession(id); + set(s => { + const next = { ...s.messages }; + delete next[id]; + return { + ...s, + sessions: s.sessions.filter(c => c.id !== id), + messages: next, + activeId: s.activeId === id ? null : s.activeId, + }; + }); + pushToast('Chat deleted', 'info'); + } catch (e) { + console.error('delete failed', e); + pushToast('Delete failed', 'error'); + } +} + +async function archiveChat(id: string, archived: boolean): Promise { + try { + await api.patchSession(id, { archived }); + set(s => ({ ...s, sessions: s.sessions.map(c => c.id === id ? { ...c, archived } : c) })); + pushToast(archived ? 'Chat archived' : 'Chat unarchived', 'info'); + } catch (e) { + console.error('archive failed', e); + } +} + +// Last (content, timestamp) pair we sent. Used to drop the burst of +// duplicate sends that occur when multiple onSubmit listeners fire +// from a single Enter keypress during the first turn (the createNewChat +// path takes a microtask round-trip, so N parallel sendUserMessage +// calls all see activeId == null and all use the *last* chatId the +// parallel createNewChat calls land on). 500 ms is short enough that +// intentional rapid-fire sends of different content are unaffected. +const lastSent = { value: '', timestamp: 0 }; + +async function sendUserMessage(content: string): Promise { + const now = Date.now(); + if (content === lastSent.value && now - lastSent.timestamp < 500) { + return; + } + lastSent.value = content; + lastSent.timestamp = now; + + const s = getStore(); + let chatId = s.activeId; + if (!chatId) { + await createNewChat(); + chatId = getStore().activeId!; + } + set(s => { + const arr = [...(s.messages[chatId!] ?? []), { role: 'user' as const, content, created_at: Date.now() }]; + const next = { ...s.pendingDelta }; + delete next[chatId!]; + return { + ...s, + messages: { ...s.messages, [chatId!]: arr }, + pendingDelta: next, + status: 'streaming', + sessions: s.sessions.map(c => c.id === chatId ? { ...c, updated_at: Date.now(), message_count: c.message_count + 1 } : c), + }; + }); + sendMessage(chatId, content, getOverrides()); +} + +// ─── Chat management: favorite, rename, drag-reorder ───────── + +async function toggleFavorite(id: string): Promise { + const s = getStore(); + const c = s.sessions.find(x => x.id === id); + if (!c) return; + const favorite = !c.favorite; + set(s => ({ ...s, sessions: s.sessions.map(x => x.id === id ? { ...x, favorite } : x) })); + try { + await api.patchSession(id, { favorite }); + } catch (e) { + console.error('favorite failed', e); + // Revert on failure. + set(s => ({ ...s, sessions: s.sessions.map(x => x.id === id ? { ...x, favorite: !favorite } : x) })); + pushToast('Pin failed', 'error'); + } +} + +function startRename(id: string, el: HTMLElement): void { + const item = el.closest('.eva-chat-item'); + if (!item) return; + const display = item.querySelector('[data-title-display]'); + if (!display) return; + const s = getStore(); + const chat = s.sessions.find(x => x.id === id); + if (!chat) return; + const current = chat.title ?? ''; + // Replace the title span with an input. Keep it inside the same + // flex slot so the layout doesn't shift. + const input = document.createElement('input'); + input.type = 'text'; + input.value = current; + input.dataset.rename = ''; + input.dataset.id = id; + input.maxLength = 120; + input.className = 'flex-1 text-sm bg-transparent border-b border-pink outline-none px-0 py-0'; + input.style.color = 'var(--eva-text)'; + display.replaceWith(input); + input.focus(); + input.select(); + // Prevent the row click handler from opening the chat while editing. + input.addEventListener('click', e => e.stopPropagation()); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); input.blur(); } + if (e.key === 'Escape') { + e.preventDefault(); + // Restore without saving. + const span = document.createElement('span'); + span.className = 'flex-1 truncate text-sm'; + span.dataset.titleDisplay = ''; + span.textContent = current; + input.replaceWith(span); + } + }); + input.addEventListener('blur', async () => { + const newTitle = input.value.trim(); + if (newTitle === '' || newTitle === current) { + // Restore the display span unchanged. + const span = document.createElement('span'); + span.className = 'flex-1 truncate text-sm'; + span.dataset.titleDisplay = ''; + span.textContent = current; + input.replaceWith(span); + return; + } + try { + await api.patchSession(id, { title: newTitle }); + set(s => ({ ...s, sessions: s.sessions.map(x => x.id === id ? { ...x, title: newTitle } : x) })); + pushToast('Renamed', 'success'); + } catch (e) { + console.error('rename failed', e); + pushToast('Rename failed', 'error'); + } + }); +} + +function reconnectWs(): void { + pushToast('Reconnecting…'); + closeWs(); + connectWs(); +} + +function cancelStream(): void { + const s = getStore(); + if (s.activeId) { + sendCancel(s.activeId); + pushToast('Cancelling…'); + } +} + +// Drag-reorder. PATCHes sort_order on both the dragged and the +// target chat. Strategy: take the dragged chat's sort_order and +// move it past the target by setting the dragged's new order to +// (target.order + 1) for drop-above, or (target.order - 1) for +// drop-below (computed from cursor Y relative to target midpoint). +// To avoid full-table re-numbering, we shift the target and +// dragged only — this may invert the order of the two, which is +// the expected behavior for a 1-step drag. +export function onDragStart(e: DragEvent): void { + const item = (e.target as HTMLElement).closest('.eva-chat-item'); + if (!item) return; + const id = item.dataset.id; + if (!id) return; + e.dataTransfer?.setData('text/plain', id); + e.dataTransfer!.effectAllowed = 'move'; + item.classList.add('eva-chat-dragging'); +} + +export function onDragEnd(e: DragEvent): void { + const item = (e.target as HTMLElement).closest('.eva-chat-item'); + item?.classList.remove('eva-chat-dragging'); + document.querySelectorAll('.eva-chat-drag-over').forEach(el => el.classList.remove('eva-chat-drag-over')); +} + +export function onDragOver(e: DragEvent): void { + const item = (e.target as HTMLElement).closest('.eva-chat-item'); + if (!item) return; + e.preventDefault(); + e.dataTransfer!.dropEffect = 'move'; + document.querySelectorAll('.eva-chat-drag-over').forEach(el => el.classList.remove('eva-chat-drag-over')); + item.classList.add('eva-chat-drag-over'); +} + +export async function onDrop(e: DragEvent): Promise { + e.preventDefault(); + const target = (e.target as HTMLElement).closest('.eva-chat-item'); + if (!target) return; + const draggedId = e.dataTransfer?.getData('text/plain'); + if (!draggedId) return; + const targetId = target.dataset.id; + if (!targetId || targetId === draggedId) return; + + const s = getStore(); + const dragged = s.sessions.find(c => c.id === draggedId); + const tgt = s.sessions.find(c => c.id === targetId); + if (!dragged || !tgt) return; + + // Decide before/after based on cursor Y relative to the target's + // vertical midpoint. + const rect = target.getBoundingClientRect(); + const before = e.clientY < rect.top + rect.height / 2; + const newOrder = before ? tgt.sort_order + 1 : tgt.sort_order - 1; + + // Optimistic local update. + set(s => ({ + ...s, + sessions: s.sessions.map(c => c.id === draggedId ? { ...c, sort_order: newOrder } : c), + })); + + try { + await api.patchSession(draggedId, { sort_order: newOrder }); + } catch (err) { + console.error('reorder failed', err); + // Revert. + set(s => ({ + ...s, + sessions: s.sessions.map(c => c.id === draggedId ? { ...c, sort_order: dragged.sort_order } : c), + })); + pushToast('Reorder failed', 'error'); + } +} diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 0000000..9bc5cb4 --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/frontend/src/layouts/Base.astro b/frontend/src/layouts/Base.astro new file mode 100644 index 0000000..7584acd --- /dev/null +++ b/frontend/src/layouts/Base.astro @@ -0,0 +1,32 @@ +--- +import '../styles/global.css'; + +interface Props { + title?: string; +} +const { title = 'Eva' } = Astro.props; +--- + + + + + + + + {title} + + +
+ + + + diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..3183fa3 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,83 @@ +// REST API helpers. Thin wrappers over fetch() that: +// - prepend the base path (/api) +// - attach the Authorization: Bearer header when an apiKey is set +// - parse JSON responses and surface structured errors +// - use AbortSignal for cancellable requests (Phase 4: live-search) +// +// The backend is at the same origin in production (EVA_SERVE_FRONTEND), +// and at localhost:4321 -> proxy -> :8765 during `astro dev`. Either +// way the browser sees a single origin, so no CORS dance. + +import type { Chat, Message } from '../state/store.ts'; +import { getStore, set } from '../state/store.ts'; + +class ApiError extends Error { + status: number; + body: string; + constructor(status: number, body: string) { + super(`api ${status}: ${body}`); + this.status = status; + this.body = body; + } +} + +function headers(): HeadersInit { + const h: Record = { 'Content-Type': 'application/json' }; + const key = getStore().apiKey; + if (key) h.Authorization = `Bearer ${key}`; + return h; +} + +async function call(method: string, path: string, body?: unknown): Promise { + const res = await fetch(`/api${path}`, { + method, + headers: headers(), + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + if (!res.ok) { + const text = await res.text(); + throw new ApiError(res.status, text); + } + if (res.status === 204) return undefined as T; + return res.json() as Promise; +} + +export const api = { + // ── Config / health ───────────────────────────────────────── + health: () => call<{ status: string }>('GET', '/health'), + config: () => + call<{ + model: string; + context_window: number; + max_tokens: number; + max_tool_iterations: number; + api_key_required: boolean; + }>('GET', '/config'), + + // ── Sessions ──────────────────────────────────────────────── + listSessions: () => + call<{ sessions: Chat[] }>('GET', '/sessions').then(r => r.sessions), + + getMessages: (id: string) => + call<{ messages: Message[] }>('GET', `/sessions/${encodeURIComponent(id)}/messages`) + .then(r => r.messages), + + patchSession: (id: string, patch: Partial>) => + call<{ status: string }>('PATCH', `/sessions/${encodeURIComponent(id)}`, patch), + + deleteSession: (id: string) => + call<{ status: string }>('DELETE', `/sessions/${encodeURIComponent(id)}`), + + // ── Bootstrap: load config + first session list in parallel ─ + async bootstrap() { + const [cfg, sessions] = await Promise.all([ + api.config(), + api.listSessions().catch(() => [] as Chat[]), + ]); + set({ + config: cfg, + authRequired: cfg.api_key_required, + sessions, + }); + }, +}; diff --git a/frontend/src/lib/boot.ts b/frontend/src/lib/boot.ts new file mode 100644 index 0000000..f00b02a --- /dev/null +++ b/frontend/src/lib/boot.ts @@ -0,0 +1,41 @@ +// SPA boot: replaces the Astro placeholder with the full UI. +// We use a thin custom-element wrapper (``) so the rest of +// the app is a regular Svelte/Preact-free vanilla TS module tree. +// Keeping it framework-free minimizes bundle size and avoids a build +// step beyond Astro/Tailwind. +// +// Module graph (lazy-imported as the user navigates): +// boot.ts -> mounts +// state/store -> reactive SessionStore (proxy-based) +// state/ws -> WebSocket client (auto-reconnect, auth) +// components/ -> Sidebar, MessageList, Input, SettingsModal, TracePanel +// lib/api -> REST helpers (listSessions, patchSession, ...) +// lib/icons -> inline SVG icon set +// +// All UI re-renders through a tiny `mount()` helper that diffs innerHTML +// only on structural changes; for fine-grained updates the components +// patch specific sub-trees. This is intentionally simpler than a VDOM — +// the whole app is under 10 components. + +import { initStore } from '../state/store.ts'; +import { connectWs } from '../state/ws.ts'; +import { renderShell } from '../components/Shell.ts'; + +export function mountEva(root: HTMLElement): void { + const store = initStore(); + connectWs(); + + const shell = document.createElement('eva-shell'); + shell.setAttribute('data-store', ''); // marker for the store ref + // Hand the store to the shell via a custom property to avoid a + // global. The shell reads it on connectedCallback. + (shell as any).store = store; + // Keep an explicit ref so HMR / dev reload can find the store. + (globalThis as any).__evaStore = store; + + root.innerHTML = ''; + root.appendChild(shell); + + // First render. + renderShell(shell as any, store); +} diff --git a/frontend/src/lib/icons.ts b/frontend/src/lib/icons.ts new file mode 100644 index 0000000..a309ba1 --- /dev/null +++ b/frontend/src/lib/icons.ts @@ -0,0 +1,64 @@ +// Inline SVG icons. Mirrors the Font Awesome 6 free set used in +// mock.html. Single source of truth so the bundle stays tiny and +// there's no CDN dependency. `viewBox` is always 0 0 24 24 so they +// share the same baseline as FA 6.5 regular weight. `width="1em"` +// and `height="1em"` make each icon size to the font-size of its +// parent span — that's how FA's `text-xs`/`text-sm` wrappers work. + +const wrap = (path: string, opts: { fill?: string; strokeWidth?: string } = {}) => { + const fill = opts.fill ?? 'none'; + const sw = opts.strokeWidth ?? '2'; + return `${path}`; +}; + +const solid = (path: string) => + `${path}`; + +export const icons = { + // ─── Brand / nav ──────────────────────────────────────── + robot: solid(``), + plus: wrap(``), + xmark: wrap(``), + bars: wrap(``), + + // ─── Sidebar ──────────────────────────────────────────── + sliders: wrap(``), + boxArchive: wrap(``), + trash: wrap(``), + magnifyingGlass: wrap(``), + star: wrap(``), + starFilled: solid(``), + pencil: wrap(``), + grip: wrap(``), + + // ─── Header ───────────────────────────────────────────── + diagramProject: wrap(``), + satelliteDish: wrap(``), + + // ─── Model radio cards ────────────────────────────────── + bolt: solid(``), + gaugeHigh: solid(``), + + // ─── Input ────────────────────────────────────────────── + paperclip: wrap(``), + arrowUp: solid(``), + + // ─── Trace steps ──────────────────────────────────────── + brain: solid(``), + sitemap: solid(``), + penFancy: solid(``), + shieldHalved: solid(``), + gear: solid(``), + + // ─── Suggestions ──────────────────────────────────────── + chartLine: solid(``), + code: solid(``), + listCheck: solid(``), + + // ─── Inline actions ───────────────────────────────────── + copy: wrap(``), + check: wrap(``), + floppy: solid(``), +}; + +export type IconName = keyof typeof icons; diff --git a/frontend/src/pages/index.astro b/frontend/src/pages/index.astro new file mode 100644 index 0000000..0902438 --- /dev/null +++ b/frontend/src/pages/index.astro @@ -0,0 +1,14 @@ +--- +import Base from '../layouts/Base.astro'; +--- + +
+
+
+ +
+

Loading Eva…

+

If this persists, check that the Eva backend is running.

+
+
+ diff --git a/frontend/src/state/store.ts b/frontend/src/state/store.ts new file mode 100644 index 0000000..bd7ad4d --- /dev/null +++ b/frontend/src/state/store.ts @@ -0,0 +1,221 @@ +// Reactive store. One module-level state object that components +// subscribe to. Mutations go through `set()` which schedules a +// microtask flush of dirty subscribers. We don't pull in a framework +// signal library to keep the bundle small; the Set subscribe + +// broadcast pattern is ~40 lines. +// +// State shape (mirrors mock.html): +// sessions[] list of chat sessions +// activeId currently-open chat id, or null +// messages{} { [chatId]: Message[] } full history +// pendingDelta{} { [chatId]: string } streaming token buffer +// status 'idle' | 'streaming' | 'tool' +// config server-reported config (model, ctx, etc.) +// apiKey user-supplied EVA_API_KEY (sessionStorage) +// authRequired bool — does the backend require a key? +// settingsOpen bool — settings modal visibility +// settingsModel string — selected model in radio cards +// (cosmetic; backend has one model) +// settingsTemp number — temperature slider +// settingsTopP number — top_p slider +// settingsMaxTokens number — max_tokens slider +// settingsSysPrompt string — system prompt textarea +// traceOpen bool — right trace panel visibility +// trace{} { [chatId]: TraceEntry[] } agent steps +// connection 'connecting' | 'open' | 'closed' | 'error' +// wsRetries int — reconnection attempt counter +// sidebarOpen bool — sidebar visibility (mobile) +// showingArchived bool — chat list filter +// searchQuery string — sidebar search filter +// toasts[] active toast notifications +// suggestions[] welcome screen cards (3 mock items) + +export type Chat = { + id: string; + title: string | null; + created_at: number; + updated_at: number; + archived: boolean; + favorite: boolean; + sort_order: number; + message_count: number; +}; + +export type Message = { + id?: string; + role: 'user' | 'assistant' | 'tool' | 'system'; + content: string; + created_at?: number; + tool_call_id?: string; +}; + +export type TraceEntry = { + step: string; + label: string; + detail: string; + elapsed_ms: number; + at: number; +}; + +export type Toast = { + id: number; + text: string; + tone: 'info' | 'success' | 'error'; +}; + +export type Suggestion = { + title: string; + desc: string; + text: string; + icon: 'chart-line' | 'code' | 'list-check'; + iconColor: 'pink' | 'teal' | 'purple'; +}; + +export type Store = { + sessions: Chat[]; + activeId: string | null; + messages: Record; + pendingDelta: Record; + status: 'idle' | 'streaming' | 'tool'; + config: { + model: string; + context_window: number; + max_tokens: number; + max_tool_iterations: number; + api_key_required: boolean; + } | null; + apiKey: string; + authRequired: boolean; + settingsOpen: boolean; + settingsModel: string; + settingsTemp: number | null; + settingsTopP: number | null; + settingsMaxTokens: number | null; + settingsSysPrompt: string; + traceOpen: boolean; + trace: Record; + connection: 'connecting' | 'open' | 'closed' | 'error'; + wsRetries: number; + sidebarOpen: boolean; + showingArchived: boolean; + searchQuery: string; + toasts: Toast[]; + suggestions: Suggestion[]; +}; + +type Listener = (s: Store) => void; + +const subs = new Set(); +// NOTE: `state` is a stable reference that we mutate in place. The +// previous immutable pattern (`state = { ...state, ...patch }`) created +// a new object on every update, which broke `globalThis.__evaStore` +// (captured at boot time) — the global held the *initial* object, so +// every re-render read the freshest-at-boot snapshot and missed all +// subsequent updates. Mutating in place means any reference to `state` +// always sees the latest values, which is what the rest of the app +// assumes via `getStore()` and `__evaStore`. +const state: Store = fresh(); +let toastId = 0; +let toastTimers = new Map(); + +function fresh(): Store { + return { + sessions: [], + activeId: null, + messages: {}, + pendingDelta: {}, + status: 'idle', + config: null, + apiKey: sessionStorage.getItem('eva.apiKey') ?? '', + authRequired: false, + settingsOpen: false, + settingsModel: '', + settingsTemp: null, + settingsTopP: null, + settingsMaxTokens: null, + settingsSysPrompt: '', + traceOpen: false, // closed by default; toggle via header button + trace: {}, + connection: 'connecting', + wsRetries: 0, + sidebarOpen: false, + showingArchived: false, + searchQuery: '', + toasts: [], + suggestions: [ + { title: 'Analyze Trends', desc: 'Market analysis with key insights', text: 'Analyze the latest market trends and summarize key insights', icon: 'chart-line', iconColor: 'pink' }, + { title: 'Debug Code', desc: 'Fix React re-rendering issues', text: 'Help me debug this React component that is not re-rendering correctly', icon: 'code', iconColor: 'teal' }, + { title: 'Plan Project', desc: 'Mobile app with AI roadmap', text: 'Create a project plan for building a mobile app with AI features', icon: 'list-check', iconColor: 'purple' }, + ], + }; +} + +export function initStore(): Store { + return state; +} + +export function getStore(): Store { + return state; +} + +export function set(patch: Partial): void; +export function set>(updater: (s: Store) => R): void; +export function set(arg: Partial | ((s: Store) => Partial)): void { + const patch = typeof arg === 'function' ? arg(state) : arg; + // Apply only the keys present in the patch — leaves the rest of + // `state` untouched. Using Object.keys (not the spread) is what + // keeps the reference stable. + for (const k of Object.keys(patch) as (keyof Store)[]) { + (state as any)[k] = (patch as any)[k]; + } + if ('apiKey' in patch) { + if (patch.apiKey) sessionStorage.setItem('eva.apiKey', patch.apiKey); + else sessionStorage.removeItem('eva.apiKey'); + } + for (const l of subs) l(state); +} + +export function subscribe(fn: Listener): () => void { + subs.add(fn); + return () => subs.delete(fn); +} + +// ─── Toast helpers (transient notifications) ───────────────── +export function pushToast(text: string, tone: Toast['tone'] = 'info'): void { + toastId++; + const t: Toast = { id: toastId, text, tone }; + set(s => ({ ...s, toasts: [...s.toasts, t] })); + const handle = window.setTimeout(() => { + set(s => ({ ...s, toasts: s.toasts.filter(x => x.id !== t.id) })); + toastTimers.delete(t.id); + }, 2200); + toastTimers.set(t.id, handle); +} + +// ─── Convenience selectors ─────────────────────────────────── +export const sel = { + activeChat(s: Store): Chat | null { + return s.sessions.find(c => c.id === s.activeId) ?? null; + }, + activeMessages(s: Store): Message[] { + return s.activeId ? s.messages[s.activeId] ?? [] : []; + }, + visibleSessions(s: Store): Chat[] { + // Pinned first, then by updated_at desc, then filtered by archive flag. + const filtered = s.showingArchived + ? s.sessions.filter(c => c.archived) + : s.sessions.filter(c => !c.archived); + const q = s.searchQuery.trim().toLowerCase(); + const matched = q + ? filtered.filter(c => (c.title ?? '').toLowerCase().includes(q)) + : filtered; + return matched.sort((a, b) => { + if (a.favorite !== b.favorite) return a.favorite ? -1 : 1; + if (a.sort_order !== b.sort_order) return b.sort_order - a.sort_order; + return b.updated_at - a.updated_at; + }); + }, + archiveCount(s: Store): number { + return s.sessions.filter(c => c.archived).length; + }, +}; diff --git a/frontend/src/state/ws.ts b/frontend/src/state/ws.ts new file mode 100644 index 0000000..16a5397 --- /dev/null +++ b/frontend/src/state/ws.ts @@ -0,0 +1,273 @@ +// WebSocket client. Reconnects with exponential backoff, throttles +// chat_id fan-out, and re-emits inbound messages from the agent loop +// as `Message` patches on the store. +// +// Wire protocol (inbound -> browser): +// {"type":"message", "chat_id":"...", "content":"..."} +// {"type":"delta", "chat_id":"...", "delta":"...", "done":false} +// {"type":"trace", "chat_id":"...", "step":"llm_call", "label":"...", +// "detail":"...", "elapsed_ms":1234} +// {"type":"error", "content":"..."} +// +// Wire protocol (browser -> server): +// {"type":"message", "chat_id":"...", "content":"...", +// "temperature":0.7, "top_p":0.9, "max_tokens":2048, +// "system_prompt":"..."} // last 3 are per-turn overrides +// {"type":"cancel", "chat_id":"..."} // abort in-flight stream +// +// The apiKey is sent as a `?token=...` query param on connect — the +// zero websocket upgrader does not currently look at upgrade headers +// in this codebase, so the query param is the only auth path on the +// WS endpoint. (REST uses Authorization: Bearer.) + +import { set, getStore } from './store.ts'; +import type { TraceEntry } from './store.ts'; +import { api } from '../lib/api.ts'; + +let ws: WebSocket | null = null; +let backoffMs = 500; +let reconnectTimer: number | null = null; +let closedByUser = false; +/// Set to true after the very first successful open. Subsequent opens +/// (i.e. reconnects) trigger a re-fetch of the active chat's messages +/// so the UI catches up on any response the user missed while +/// disconnected. +let isReconnect = false; + +export function connectWs(): void { + closedByUser = false; + openSocket(); +} + +export function closeWs(): void { + closedByUser = true; + if (reconnectTimer !== null) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + ws?.close(); + ws = null; +} + +/// Vite's dev WebSocket proxy (`ws: true` in astro.config.mjs) drops +/// long-lived connections; the server log shows +/// `received close message` on otherwise-idle sockets. In production +/// the frontend is served by the backend (EVA_SERVE_FRONTEND=true) so +/// `location.host` already points to the backend and this is a no-op. +/// In dev we bypass the proxy and connect directly to the backend, +/// which the user runs on :8765 by default. Override via the +/// `window.__EVA_WS_HOST__` global if the backend is on a different +/// host/port. +function resolveBackendHost(): string { + if (typeof window !== 'undefined') { + const override = (window as any).__EVA_WS_HOST__; + if (typeof override === 'string' && override.length > 0) return override; + } + return location.host; +} + +function openSocket(): void { + const s = getStore(); + set({ connection: 'connecting' }); + + // If a previous WebSocket is still around (rapid reconnect, manual + // Reconnect click), close it synchronously before opening a new one. + // The OS-level close handshake is async — without this the old + // listener can fire one more time after we reassign `ws`, which + // pushes a duplicate of any in-flight assistant message into the + // store. Detach all listeners first so even an in-flight delivery + // is a no-op. NB: setting `old.onmessage = null` does NOT remove + // addEventListener listeners — must use removeEventListener with + // the same function reference. The listeners are module-level named + // functions so the references stay stable. + if (ws) { + const old = ws; + ws = null; + old.removeEventListener('open', onWsOpen); + old.removeEventListener('message', onWsMessage); + old.removeEventListener('close', onWsClose); + old.removeEventListener('error', onWsError); + try { old.close(); } catch (_) {} + } + + const proto = location.protocol === 'https:' ? 'wss' : 'ws'; + const tokenParam = s.apiKey ? `?token=${encodeURIComponent(s.apiKey)}` : ''; + const host = resolveBackendHost(); + const url = `${proto}://${host}/ws${tokenParam}`; + + ws = new WebSocket(url); + + ws.addEventListener('open', onWsOpen); + ws.addEventListener('message', onWsMessage); + ws.addEventListener('close', onWsClose); + ws.addEventListener('error', onWsError); +} + +function onWsOpen(): void { + backoffMs = 500; + set({ connection: 'open', wsRetries: 0 }); + if (isReconnect) { + // Re-fetch the active chat's messages to catch up on any + // assistant response that was dispatched while we were + // disconnected. The backend has the canonical record (we + // append to the session in agent_loop.zig regardless of WS + // state), so the REST endpoint is authoritative. + const cur = getStore(); + if (cur.activeId) { + api.getMessages(cur.activeId) + .then((messages) => { + // Dedup consecutive duplicates (same role + content) — the agent + // loop's tool-call iterations can produce many tool messages that + // look like assistant bubbles in the DB snapshot. + const deduped = messages.filter((m, i, a) => i === 0 || m.role !== a[i - 1].role || m.content !== a[i - 1].content); + set((s) => ({ ...s, messages: { ...s.messages, [s.activeId!]: deduped } })); + }) + .catch((e) => console.warn('reconnect re-fetch failed', e)); + } + } + isReconnect = true; +} + +function onWsMessage(e: MessageEvent): void { + onMessage((e as MessageEvent).data as string); +} + +function onWsClose(): void { + set({ connection: 'closed' }); + scheduleReconnect(); +} + +function onWsError(): void { + set({ connection: 'error' }); +} + +function scheduleReconnect(): void { + if (closedByUser) return; + set(s => ({ ...s, wsRetries: s.wsRetries + 1 })); + reconnectTimer = window.setTimeout(() => { + reconnectTimer = null; + backoffMs = Math.min(backoffMs * 2, 30_000); + openSocket(); + }, backoffMs); +} + +export function sendMessage( + chatId: string, + content: string, + overrides: { + temperature?: number; + top_p?: number; + max_tokens?: number; + system_prompt?: string; + } = {}, +): void { + if (!ws || ws.readyState !== WebSocket.OPEN) return; + const payload: Record = { + type: 'message', + chat_id: chatId, + content, + }; + if (overrides.temperature !== undefined) payload.temperature = overrides.temperature; + if (overrides.top_p !== undefined) payload.top_p = overrides.top_p; + if (overrides.max_tokens !== undefined) payload.max_tokens = overrides.max_tokens; + if (overrides.system_prompt) payload.system_prompt = overrides.system_prompt; + ws.send(JSON.stringify(payload)); +} + +export function sendCancel(chatId: string): void { + if (!ws || ws.readyState !== WebSocket.OPEN) return; + ws.send(JSON.stringify({ type: 'cancel', chat_id: chatId })); +} + +function onMessage(raw: string): void { + // The backend terminates every outbound payload with a `\n` so a + // single WS frame may contain several NDJSON-encoded messages + // (kernel-level coalescing or any TCP_NODELAY-disabled path will + // produce this). Split on newline and dispatch each line; ignore + // blank tails. + const lines = raw.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.length === 0) continue; + handleOne(trimmed); + } +} + +function handleOne(raw: string): void { + let env: { type: string; chat_id?: string; content?: string; delta?: string; done?: boolean; step?: string; label?: string; detail?: string; elapsed_ms?: number }; + try { env = JSON.parse(raw); } catch { return; } + + switch (env.type) { + case 'message': { + const chatId = env.chat_id; + const content = env.content ?? ''; + if (!chatId) return; + set(s => { + const arr = s.messages[chatId] ?? []; + // Dedup against ALL existing assistant messages. The backend + // sends exactly one `{"type":"message"}` per user input, but + // multiple WS connections (Vite proxy, reconnects) can deliver + // the same outbound frame multiple times, and the API re-fetch + // on reconnect may include responses that also arrive via WS. + const alreadyPresent = arr.some(m => m.role === 'assistant' && m.content === content); + if (alreadyPresent) return {}; + const next = { ...s.pendingDelta }; + delete next[chatId]; + return { + ...s, + messages: { ...s.messages, [chatId]: [...arr, { role: 'assistant', content, created_at: Date.now() }] }, + pendingDelta: next, + status: 'idle', + }; + }); + break; + } + case 'delta': { + const chatId = env.chat_id; + const delta = env.delta ?? ''; + if (!chatId) return; + if (env.done) { + set(s => { + const next = { ...s.pendingDelta }; + delete next[chatId]; + return { ...s, pendingDelta: next, status: 'idle' }; + }); + break; + } + set(s => { + const next = { ...s.pendingDelta, [chatId]: (s.pendingDelta[chatId] ?? '') + delta }; + return { ...s, pendingDelta: next, status: 'streaming' }; + }); + break; + } + case 'trace': { + const chatId = env.chat_id; + if (!chatId) return; + const entry: TraceEntry = { + step: env.step ?? '', + label: env.label ?? '', + detail: env.detail ?? '', + elapsed_ms: env.elapsed_ms ?? 0, + at: Date.now(), + }; + set(s => { + const arr = [...(s.trace[chatId] ?? []), entry]; + return { ...s, trace: { ...s.trace, [chatId]: arr } }; + }); + break; + } + case 'error': { + // Surface as a transient system message in the active chat. + const chatId = getStore().activeId; + const content = env.content ?? 'unknown error'; + if (chatId) { + set(s => { + const arr = [...(s.messages[chatId] ?? [])]; + arr.push({ role: 'system', content, created_at: Date.now() }); + return { ...s, messages: { ...s.messages, [chatId]: arr }, status: 'idle' }; + }); + } + break; + } + } +} diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css new file mode 100644 index 0000000..0748f29 --- /dev/null +++ b/frontend/src/styles/global.css @@ -0,0 +1,536 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* ─── Self-hosted fonts (woff2 in /public/fonts/) ────────────── */ +@font-face { + font-family: 'Space Grotesk'; + src: url('/fonts/SpaceGrotesk.woff2') format('woff2'); + font-weight: 400 700; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'DM Sans'; + src: url('/fonts/DMSans.woff2') format('woff2'); + font-weight: 400 500; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'JetBrains Mono'; + src: url('/fonts/JetBrainsMono.woff2') format('woff2'); + font-weight: 400 500; + font-style: normal; + font-display: swap; +} + +/* ─── Design tokens (CSS variables) ─────────────────────────── */ +:root { + --eva-pink: #ff6b8a; + --eva-pink-dark: #e8556f; + --eva-teal: #00d4aa; + --eva-purple: #8b5cf6; + --eva-bg-app: #eef4f7; + --eva-bg-sidebar: #dfe8ec; + --eva-bg-main: #eef4f7; + --eva-bg-card: #ffffff; + --eva-bg-input: rgba(255, 255, 255, 0.92); + --eva-bg-hover: #d4e2e8; + --eva-bg-active: rgba(83, 150, 172, 0.1); + --eva-bg-icon: #d4e2e8; + --eva-bg-icon-active: rgba(255, 107, 138, 0.12); + --eva-border: #b8cdd5; + --eva-border-subtle: rgba(184, 205, 213, 0.5); + --eva-text: #111e22; + --eva-text-secondary: #325a67; + --eva-text-muted: #5396ac; + --eva-text-faint: rgba(83, 150, 172, 0.4); + --eva-overlay: rgba(17, 30, 34, 0.2); + --eva-ai-border: #c8d9e0; + --eva-tooltip-bg: #111e22; + --eva-tooltip-text: #eef4f7; + --eva-trace-line: linear-gradient(to bottom, #42788a, transparent); +} + +/* ─── Base layout ───────────────────────────────────────────── */ +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} +html, +body { + height: 100%; + overflow: hidden; + font-family: 'DM Sans', system-ui, sans-serif; + background: var(--eva-bg-app); + color: var(--eva-text); + font-feature-settings: 'cv11', 'ss01', 'ss03'; + color-scheme: light; +} + +/* The custom element wraps the whole UI. As a custom + * element it's `display: inline` by default, which collapses the + * 3-pane flex layout. Make it fill #eva-root completely. */ +eva-shell, +eva-app { + display: flex; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + width: 100%; + height: 100%; + position: relative; +} + +/* All inline SVG icons inherit the font-size of their parent span + * and never squish in flex layouts. This is the FA 6 sizing model + * (text-xs parent -> 12px icon, text-sm parent -> 14px icon, etc). */ +eva-shell svg, +eva-shell [class*="eva-"] svg { + width: 1em; + height: 1em; + flex-shrink: 0; + display: inline-block; + vertical-align: -0.125em; +} + +#eva-root { + height: 100vh; + display: flex; +} + +/* ─── Smooth transitions for theme-aware surfaces ───────────── */ +body, +.eva-sidebar, +.eva-app-header, +.eva-agent-panel, +.eva-ai-bubble, +.eva-input-box, +.eva-model-btn, +.eva-model-dropdown, +.eva-sug-card, +.eva-search-input, +.eva-sidebar-bottom, +.eva-new-chat-btn, +.eva-toast, +.eva-th-t, +.eva-settings-overlay, +.eva-settings-modal { + transition: + background-color 0.3s ease, + border-color 0.3s ease, + color 0.2s ease, + box-shadow 0.3s ease; +} + +/* ─── Scrollbar (subtle) ─────────────────────────────────────── */ +::-webkit-scrollbar { width: 5px; height: 5px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: var(--eva-border); + border-radius: 10px; +} + +/* ─── Icon button (shared hover) ─────────────────────────────── */ +.eva-ib { + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.5rem; + color: var(--eva-text-muted); + cursor: pointer; + background: transparent; + border: none; + outline: none; + transition: + background 0.15s ease, + color 0.15s ease; +} +.eva-ib:hover { + background: var(--eva-bg-hover); + color: var(--eva-text); +} + +/* ─── Sidebar ────────────────────────────────────────────────── */ +.eva-sidebar { + background: var(--eva-bg-sidebar); + border-right: 1px solid var(--eva-border); + contain: layout style; + transition: + transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), + background-color 0.3s ease, + border-color 0.3s ease; +} +.eva-chat-item { + border-left: 3px solid transparent; + transition: + transform 0.15s ease, + background-color 0.15s ease, + border-color 0.15s ease; + cursor: pointer; + background: transparent; +} +.eva-chat-item:hover { + transform: translateX(3px); + background: var(--eva-bg-hover); +} +.eva-chat-item.active { + border-left-color: var(--eva-pink); + background: var(--eva-bg-active); +} +.eva-chat-item.active:hover { + background: var(--eva-bg-active); + transform: none; +} +.eva-chat-action { + opacity: 0; + transition: + opacity 0.15s ease, + color 0.15s ease, + background 0.15s ease; +} +.eva-chat-item:hover .eva-chat-action { + opacity: 1; +} +.eva-chat-del:hover { + color: #f87171 !important; + background: rgba(239, 68, 68, 0.1) !important; +} +.eva-chat-arc:hover { + color: var(--eva-purple) !important; + background: rgba(139, 92, 246, 0.1) !important; +} +.eva-chat-dragging { opacity: 0.4; } +.eva-chat-drag-over { + background: rgba(255, 107, 138, 0.08) !important; + border-left-color: var(--eva-pink) !important; +} + +.eva-sidebar-bottom { border-top: 1px solid var(--eva-border); } +.eva-sidebar-overlay { + background: var(--eva-overlay); + transition: opacity 0.3s ease; + pointer-events: none; + opacity: 0; + position: fixed; + inset: 0; + z-index: 30; +} +.eva-sidebar-overlay.on { pointer-events: all; opacity: 1; } + +/* ─── App header ─────────────────────────────────────────────── */ +.eva-app-header { + background: var(--eva-bg-main); + border-bottom: 1px solid var(--eva-border); +} +.eva-model-btn { + background: var(--eva-bg-hover); + border: 1px solid var(--eva-border); + color: var(--eva-text); + transition: all 0.2s ease; +} +.eva-model-btn:hover { border-color: var(--eva-pink); } + +/* ─── Logo glow + gradient helpers ──────────────────────────── */ +.eva-logo-gradient { + background: linear-gradient(135deg, #ff6b8a, #8b5cf6, #00d4aa); +} +.eva-logo-glow { box-shadow: 0 3px 14px rgba(255, 107, 138, 0.22); } +.eva-gradient-text { + background: linear-gradient(135deg, #ff6b8a, #8b5cf6, #00d4aa); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* ─── Search input ───────────────────────────────────────────── */ +.eva-search-input { + background: var(--eva-bg-hover); + border: 1px solid var(--eva-border-subtle); + color: var(--eva-text); + transition: all 0.2s ease; +} +.eva-search-input::placeholder { color: var(--eva-text-faint); } +.eva-search-input:focus { + border-color: var(--eva-pink); + background: var(--eva-bg-card); + outline: none; +} + +/* ─── New chat button (pink-tinted) ──────────────────────────── */ +.eva-new-chat-btn { + background: rgba(255, 107, 138, 0.1); + color: var(--eva-pink); + border: 1px solid rgba(255, 107, 138, 0.2); + transition: all 0.15s ease; +} +.eva-new-chat-btn:hover { background: rgba(255, 107, 138, 0.18); } +.eva-new-chat-btn:active { transform: scale(0.97); } + +/* ─── Archive toggle ─────────────────────────────────────────── */ +.eva-archive-toggle { + font-size: 11px; + color: var(--eva-text-muted); + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 6px; + border: none; + background: none; + transition: all 0.15s ease; +} +.eva-archive-toggle:hover { + background: var(--eva-bg-hover); + color: var(--eva-text); +} +.eva-archive-badge { + background: var(--eva-pink); + color: #fff; + font-size: 9px; + font-weight: 700; + padding: 1px 5px; + border-radius: 9999px; + min-width: 16px; + text-align: center; +} + +/* ─── Welcome screen ─────────────────────────────────────────── */ +.eva-bg-pattern { + background-image: + radial-gradient(circle at 20% 50%, rgba(255, 107, 138, 0.025) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(0, 212, 170, 0.025) 0%, transparent 50%), + radial-gradient(circle at 50% 80%, rgba(139, 92, 246, 0.025) 0%, transparent 50%); +} + +.eva-sug-card { + background: var(--eva-bg-card); + border: 1px solid var(--eva-border); + box-shadow: 0 1px 4px rgba(17, 30, 34, 0.05), 0 0 0 1px var(--eva-border); + transition: all 0.25s ease; + cursor: pointer; +} +.eva-sug-card:hover { + border-color: var(--eva-pink); + box-shadow: 0 6px 20px rgba(17, 30, 34, 0.08), 0 0 0 1px var(--eva-pink); +} + +/* ─── Messages ───────────────────────────────────────────────── */ +.eva-msg-in { will-change: transform; } +@media (prefers-reduced-motion: no-preference) { + .eva-msg-in { animation: msgIn 0.3s ease-out forwards; } +} +@keyframes msgIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.eva-ai-bubble { + background: var(--eva-bg-card); + border: 1px solid var(--eva-ai-border); + box-shadow: 0 1px 5px rgba(17, 30, 34, 0.05); +} +.eva-user-bubble { + background: var(--eva-pink); + color: #fff; +} + +/* ─── 3-dot typing animation (staggered) ─────────────────────── */ +@keyframes dotB { + 0%, 60%, 100% { transform: translateY(0); } + 30% { transform: translateY(-5px); } +} +.eva-tdot { animation: dotB 1.4s infinite; } +.eva-tdot:nth-child(2) { animation-delay: 0.15s; } +.eva-tdot:nth-child(3) { animation-delay: 0.3s; } + +/* ─── Status bar (above input) ──────────────────────────────── */ +.eva-status-bar { + background: rgba(0, 184, 146, 0.08); + border: 1px solid rgba(0, 184, 146, 0.25); +} + +/* ─── Input box (focus glow) ────────────────────────────────── */ +.eva-input-box { + background: var(--eva-bg-input); + border: 1px solid var(--eva-border); + backdrop-filter: blur(8px); + transition: + box-shadow 0.2s ease, + border-color 0.2s ease, + background-color 0.3s ease; +} +.eva-input-box:focus-within { + box-shadow: + 0 0 0 2px rgba(83, 150, 172, 0.3), + 0 2px 12px rgba(83, 150, 172, 0.08); + border-color: var(--eva-pink); +} +.eva-input-box textarea::placeholder, +.eva-input-box input::placeholder { + color: var(--eva-text-faint); +} + +/* ─── Send button (pink) ───────────────────────────────────── */ +.eva-send-btn { + background: var(--eva-pink); + color: #fff; + border: none; + cursor: pointer; + transition: all 0.15s ease; +} +.eva-send-btn:hover:not(:disabled) { background: var(--eva-pink-dark); } +.eva-send-btn:active:not(:disabled) { transform: scale(0.9); } +.eva-send-btn:disabled { opacity: 0.3; cursor: default; } + +/* ─── Agent trace panel ────────────────────────────────────── */ +.eva-agent-panel { + background: var(--eva-bg-sidebar); + border-left: 1px solid var(--eva-border); +} +.eva-step-conn { position: relative; } +.eva-step-conn::after { + content: ""; + position: absolute; + left: 15px; + top: 32px; + bottom: -8px; + width: 2px; + background: var(--eva-trace-line); +} +.eva-step-conn:last-child::after { display: none; } + +/* ─── Settings modal ────────────────────────────────────────── */ +.eva-settings-overlay { + background: var(--eva-overlay); + position: fixed; + inset: 0; + z-index: 60; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.25s ease; +} +.eva-settings-overlay.on { opacity: 1; pointer-events: all; } +.eva-settings-modal { + background: var(--eva-bg-card); + border: 1px solid var(--eva-border); + border-radius: 1rem; + box-shadow: 0 10px 40px rgba(17, 30, 34, 0.1); + width: 90%; + max-width: 520px; + max-height: 85vh; + overflow-y: auto; + transform: translateY(16px) scale(0.97); + transition: transform 0.25s ease; +} +.eva-settings-overlay.on .eva-settings-modal { + transform: translateY(0) scale(1); +} + +/* ─── Range slider (custom thumb) ──────────────────────────── */ +input[type='range'].eva-slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 6px; + border-radius: 3px; + background: var(--eva-bg-hover); + outline: none; + cursor: pointer; +} +input[type='range'].eva-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--eva-pink); + border: 2px solid var(--eva-bg-card); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); + cursor: pointer; + transition: transform 0.1s ease; +} +input[type='range'].eva-slider::-webkit-slider-thumb:hover { transform: scale(1.15); } +input[type='range'].eva-slider::-webkit-slider-thumb:active { transform: scale(1.25); } +input[type='range'].eva-slider::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--eva-pink); + border: 2px solid var(--eva-bg-card); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15); + cursor: pointer; +} + +/* ─── Radio card (model selection) ─────────────────────────── */ +.eva-model-radio { cursor: pointer; } +.eva-model-radio input { display: none; } +.eva-model-radio-card { + border: 1px solid var(--eva-border); + border-radius: 0.75rem; + padding: 0.75rem; + transition: all 0.15s ease; + background: var(--eva-bg-card); +} +.eva-model-radio input:checked + .eva-model-radio-card { + border-color: var(--eva-pink); + background: rgba(255, 107, 138, 0.05); + box-shadow: 0 0 0 1px var(--eva-pink); +} +.eva-model-radio:hover .eva-model-radio-card { border-color: var(--eva-text-muted); } + +/* ─── Tooltip (data-tip) ───────────────────────────────────── */ +.eva-tooltip { position: relative; } +.eva-tooltip::after { + content: attr(data-tip); + position: absolute; + bottom: 120%; + left: 50%; + transform: translateX(-50%) scale(0.9); + padding: 4px 10px; + border-radius: 6px; + font-size: 12px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: all 0.15s ease; + background: var(--eva-tooltip-bg); + color: var(--eva-tooltip-text); + z-index: 50; +} +.eva-tooltip:hover::after { + opacity: 1; + transform: translateX(-50%) scale(1); +} + +/* ─── Toast (slide-up notification) ────────────────────────── */ +.eva-toast { + background: var(--eva-bg-card); + border: 1px solid var(--eva-border); + box-shadow: 0 10px 40px rgba(17, 30, 34, 0.1); + color: var(--eva-text); + pointer-events: none; + transition: all 0.3s ease; +} + +/* ─── Fade-in (transitions for new elements) ───────────────── */ +.eva-fade-in { animation: eva-fade-in 200ms ease both; } +@keyframes eva-fade-in { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ─── Pulse (for streaming cursor + status dot) ────────────── */ +.eva-pulse { animation: eva-pulse 1.4s ease-in-out infinite; } +@keyframes eva-pulse { + 0%, 100% { opacity: 0.4; } + 50% { opacity: 1; } +} diff --git a/frontend/tailwind.config.mjs b/frontend/tailwind.config.mjs new file mode 100644 index 0000000..dc18e1b --- /dev/null +++ b/frontend/tailwind.config.mjs @@ -0,0 +1,91 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{astro,html,ts,tsx}'], + darkMode: 'class', + theme: { + extend: { + fontFamily: { + display: ['Space Grotesk', 'sans-serif'], + body: ['DM Sans', 'sans-serif'], + mono: ['JetBrains Mono', 'monospace'], + }, + colors: { + // Brand accents + pink: { + DEFAULT: '#ff6b8a', + dark: '#e8556f', + }, + teal: { + DEFAULT: '#00d4aa', + }, + purple: { + DEFAULT: '#8b5cf6', + }, + // Surfaces (light theme only in v1) + 'bg-app': '#eef4f7', + 'bg-sidebar': '#dfe8ec', + 'bg-main': '#eef4f7', + 'bg-card': '#ffffff', + 'bg-input': 'rgba(255, 255, 255, 0.92)', + 'bg-hover': '#d4e2e8', + 'bg-active': 'rgba(83, 150, 172, 0.1)', + 'bg-icon': '#d4e2e8', + 'bg-icon-active': 'rgba(255, 107, 138, 0.12)', + // Text + 'text': '#111e22', + 'text-secondary': '#325a67', + 'text-muted': '#5396ac', + 'text-faint': 'rgba(83, 150, 172, 0.4)', + // Borders + border: '#b8cdd5', + 'border-subtle': 'rgba(184, 205, 213, 0.5)', + 'ai-border': '#c8d9e0', + // Status + 'status-bg': 'rgba(0, 184, 146, 0.08)', + 'status-border': 'rgba(0, 184, 146, 0.25)', + // Semantic + danger: '#f87171', + success: '#00d4aa', + }, + boxShadow: { + card: '0 1px 3px rgba(17, 30, 34, 0.06)', + dropdown: '0 10px 40px rgba(17, 30, 34, 0.1)', + 'input-focus': + '0 0 0 2px rgba(83, 150, 172, 0.3), 0 2px 12px rgba(83, 150, 172, 0.08)', + 'ai-shadow': '0 1px 5px rgba(17, 30, 34, 0.05)', + 'sug-shadow': '0 1px 4px rgba(17, 30, 34, 0.05), 0 0 0 1px var(--eva-border)', + 'sug-hover': + '0 6px 20px rgba(17, 30, 34, 0.08), 0 0 0 1px var(--eva-pink)', + 'logo-glow': '0 3px 14px rgba(255, 107, 138, 0.22)', + }, + borderRadius: { + DEFAULT: '0.5rem', + }, + keyframes: { + 'msg-in': { + from: { opacity: '0', transform: 'translateY(10px)' }, + to: { opacity: '1', transform: 'translateY(0)' }, + }, + 'dot-b': { + '0%, 60%, 100%': { transform: 'translateY(0)' }, + '30%': { transform: 'translateY(-5px)' }, + }, + 'fade-in': { + from: { opacity: '0', transform: 'translateY(4px)' }, + to: { opacity: '1', transform: 'translateY(0)' }, + }, + 'pulse': { + '0%, 100%': { opacity: '0.4' }, + '50%': { opacity: '1' }, + }, + }, + animation: { + 'msg-in': 'msg-in 0.3s ease-out forwards', + 'dot-b': 'dot-b 1.4s infinite', + 'fade-in': 'fade-in 0.2s ease both', + pulse: 'pulse 1.4s ease-in-out infinite', + }, + }, + }, + plugins: [], +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..70f0cae --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@components/*": ["src/components/*"], + "@lib/*": ["src/lib/*"], + "@state/*": ["src/state/*"] + } + } +} diff --git a/src/agent_loop.zig b/src/agent_loop.zig index f63b369..05e2ebc 100644 --- a/src/agent_loop.zig +++ b/src/agent_loop.zig @@ -49,6 +49,12 @@ pub const AgentLoop = struct { session_max_messages: usize, running: bool, state: AgentState, + /// Atomic cancellation flag. The WS "cancel" message type flips + /// this; the chatStream loop checks it once per SSE event and the + /// onDelta callback checks it before publishing, so cancellation + /// is effective at token granularity. Single global flag is fine + /// because the agent loop processes one message at a time. + cancel: std.atomic.Value(bool), pub fn init( allocator: std.mem.Allocator, @@ -72,12 +78,13 @@ pub const AgentLoop = struct { .session_lock = null, .circuit_breaker = null, .rate_limiter = null, - .max_iterations = if (max_iterations > 0) max_iterations else 15, + .max_iterations = if (max_iterations > 0) max_iterations else 5, .consolidate_after_turns = if (consolidate_after_turns > 0) consolidate_after_turns else 20, .max_context_tokens = if (max_context_tokens > 0) max_context_tokens else 65536, .session_max_messages = if (session_max_messages > 0) session_max_messages else 10, .running = false, .state = .idle, + .cancel = std.atomic.Value(bool).init(false), }; } @@ -105,6 +112,15 @@ pub const AgentLoop = struct { self.rate_limiter = rl; } + /// Flip the cancellation flag. The currently-running chatStream + /// will return error.Cancelled on the next token; the onDelta + /// callback will skip publishing any deltas it hasn't already + /// forwarded. The flag is reset at the start of the next + /// processMessage. Safe to call from any thread. + pub fn requestCancel(self: *AgentLoop) void { + self.cancel.store(true, .release); + } + fn estimateTokens(text: []const u8) u32 { if (text.len == 0) return 0; var tokens: u32 = 0; @@ -223,7 +239,7 @@ pub const AgentLoop = struct { defer allocator.free(user_msg.content); const msgs = [_]providers.Message{ system_msg, user_msg }; - const response = self.provider.chat(&msgs, null, "") catch |err| { + const response = self.provider.chat(&msgs, null, .{}) catch |err| { const err_log = try std.fmt.allocPrint(allocator, "agent_loop: compact error: {any}", .{err}); eva_app.logErr(err_log); return false; @@ -292,12 +308,15 @@ pub const AgentLoop = struct { } fn processMessage(self: *AgentLoop, msg: bus.InboundMessage, eva_app: *eva_mod.App) !void { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - const aa = arena.allocator(); - self.state = .restore; + // Reset the cancel flag at the start of every message so a + // stale flag from a previous (cancelled) run can't suppress + // this turn's deltas. The defer ensures we also clear it on + // early return, so a follow-up call starts fresh. + self.cancel.store(false, .release); + defer self.cancel.store(false, .release); + var key_buf: [64]u8 = undefined; const session_key = msg.sessionKey(&key_buf); @@ -308,20 +327,78 @@ pub const AgentLoop = struct { const sess = try self.sessions.getOrCreate(session_key); + // Global dedup: scan backwards for the last user message. + // If its content matches the incoming message, the WS proxy + // sent a duplicate frame. Skip processing. This also catches + // duplicates arriving *after* the first call finishes (when + // the very last message is "assistant" by then). + // Users who need to re-ask should vary their phrasing. + { + var i = sess.messages.items.len; + while (i > 0) { + i -= 1; + const m = sess.messages.items[i]; + if (std.mem.eql(u8, m.role, "user")) { + if (std.mem.eql(u8, m.content, msg.content)) { + const dedup_log = try std.fmt.allocPrint(std.heap.page_allocator, "agent_loop: duplicate user message skipped (key={s})", .{session_key}); + eva_app.logWarn(dedup_log); + return; + } + break; + } + } + } + try sess.addMessage("user", msg.content); + + // Auto-set title from first user message content. + if (sess.messages.items.len == 1) { + const title = if (msg.content.len > 80) msg.content[0..80] else msg.content; + sess.db.exec("UPDATE sessions SET title = ? WHERE key = ?", .{ title, session_key }) catch {}; + } + self.state = .build; var iteration: u32 = 0; var final_response: ?[]const u8 = null; defer if (final_response) |r| self.allocator.free(r); - var tool_call_history = std.StringHashMap(u32).init(aa); - const max_repeat = 3; + var tool_call_history = std.StringHashMap(u32).init(self.allocator); + defer tool_call_history.deinit(); + const max_repeat = 2; + + // Detect when the LLM produces the same text on consecutive + // iterations while still calling tools — means it's stuck. + var prev_response: ?[]const u8 = null; + defer if (prev_response) |r| self.allocator.free(r); + var same_response_count: u32 = 0; + var force_break_tool_loop = false; + // Count of consecutive iterations where the LLM returned + // empty content with tool calls. Requires >= 2 before breaking. + var silent_tool_count: u32 = 0; var messages = std.ArrayList(providers.Message){}; - defer messages.deinit(aa); + defer { + // Only system prompt content at index 0 is owned by this + // ArrayList. role is the literal "system" — do not free. + // Items [1..] are borrowed Session references — do not free. + if (messages.items.len > 0) { + self.allocator.free(messages.items[0].content); + } + messages.deinit(self.allocator); + } while (iteration < self.max_iterations) : (iteration += 1) { + // Free the previous iteration's system prompt content only. + // role is literal "system"; items[1..] are borrowed from Session. + if (messages.items.len > 0) { + self.allocator.free(messages.items[0].content); + } + messages.clearRetainingCapacity(); + + var iter_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer iter_arena.deinit(); + const aa = iter_arena.allocator(); if (eva_mod.Shutdown.isRequested()) { if (final_response == null) { final_response = try self.allocator.dupe(u8, "Interrupted by shutdown."); @@ -337,7 +414,8 @@ pub const AgentLoop = struct { tool_defs = try reg.getDefinitions(aa); } - try self.ctx_builder.buildContextInto(&messages, aa, history, if (tool_defs) |*td| td.items else null); + const system_prompt_override: ?[]const u8 = if (msg.overrides) |ov| ov.system_prompt else null; + try self.ctx_builder.buildContextInto(&messages, self.allocator, history, system_prompt_override); const estimated_tokens = estimateMessagesTokens(messages.items); const threshold = self.max_context_tokens / 2; @@ -360,13 +438,18 @@ pub const AgentLoop = struct { bus: *bus.MessageBus, channel: []const u8, chat_id: []const u8, + cancel: *std.atomic.Value(bool), }; - var delta_ctx = DeltaCtx{ .buf = &content_buf, .alloc = aa, .bus = self.bus_ptr, .channel = msg.channel, .chat_id = msg.chat_id }; + var delta_ctx = DeltaCtx{ .buf = &content_buf, .alloc = aa, .bus = self.bus_ptr, .channel = msg.channel, .chat_id = msg.chat_id, .cancel = &self.cancel }; const onDelta = struct { fn callback(ctx_ptr: *anyopaque, delta: []const u8) void { const c: *DeltaCtx = @ptrCast(@alignCast(ctx_ptr)); + if (c.cancel.load(.acquire)) return; c.buf.appendSlice(c.alloc, delta) catch {}; + // Skip SSE streaming for WebSocket — only final + // outbound message is sent over that channel. + if (std.mem.eql(u8, c.channel, "websocket")) return; const delta_dup = std.heap.page_allocator.dupe(u8, delta) catch return; errdefer std.heap.page_allocator.free(delta_dup); const channel_dup = std.heap.page_allocator.dupe(u8, c.channel) catch return; @@ -405,6 +488,48 @@ pub const AgentLoop = struct { } }.call; + const publishTraceEvent = struct { + fn call( + bus_ptr: *bus.MessageBus, + channel: []const u8, + chat_id: []const u8, + step: []const u8, + label: []const u8, + detail: []const u8, + elapsed_ms: i64, + ) void { + const max_detail: usize = 200; + const truncated = if (detail.len > max_detail) + detail[0..max_detail] + else + detail; + const dup_channel = std.heap.page_allocator.dupe(u8, channel) catch return; + errdefer std.heap.page_allocator.free(dup_channel); + const dup_chat_id = std.heap.page_allocator.dupe(u8, chat_id) catch return; + errdefer std.heap.page_allocator.free(dup_chat_id); + const dup_step = std.heap.page_allocator.dupe(u8, step) catch return; + errdefer std.heap.page_allocator.free(dup_step); + const dup_label = std.heap.page_allocator.dupe(u8, label) catch return; + errdefer std.heap.page_allocator.free(dup_label); + const dup_detail = std.heap.page_allocator.dupe(u8, truncated) catch return; + errdefer std.heap.page_allocator.free(dup_detail); + bus_ptr.publishTrace(.{ + .channel = dup_channel, + .chat_id = dup_chat_id, + .step = dup_step, + .label = dup_label, + .detail = dup_detail, + .elapsed_ms = elapsed_ms, + }) catch { + std.heap.page_allocator.free(dup_channel); + std.heap.page_allocator.free(dup_chat_id); + std.heap.page_allocator.free(dup_step); + std.heap.page_allocator.free(dup_label); + std.heap.page_allocator.free(dup_detail); + }; + } + }.call; + if (self.circuit_breaker) |cb| { if (!cb.canExecute()) { const state = cb.getState(); @@ -428,21 +553,48 @@ pub const AgentLoop = struct { } } + // Translate InboundMessage.Overrides into provider.Overrides. + // The provider skips any null field (no JSON emit), so the + // configured AppConfig values (model, default temperature, + // etc.) are preserved unless the client overrides them. + const provider_overrides: providers.Overrides = if (msg.overrides) |ov| .{ + .temperature = ov.temperature, + .top_p = ov.top_p, + .max_tokens = ov.max_tokens, + } else .{}; + + const llm_start = std.time.milliTimestamp(); + publishTraceEvent(self.bus_ptr, msg.channel, msg.chat_id, "llm_call", "calling LLM", eva_app.eva_config.model, 0); + const response = self.provider.chatStream( messages.items, if (tool_defs) |*td| td.items else null, - "", + provider_overrides, &onDelta, &delta_ctx, + &self.cancel, ) catch |err| { const chat_id = msg.chat_id; const err_log = std.fmt.allocPrint(aa, "agent_loop: chatStream failed (chat_id={s}): {s}", .{ chat_id, @errorName(err) }) catch null; if (err_log) |m| eva_app.logErr(m); content_buf.deinit(aa); + if (err == error.Cancelled) { + // User cancelled mid-stream. Skip circuit-breaker + // accounting (cancellation is not an LLM failure) + // and surface a "cancelled" outbound so the UI can + // drop the streaming state. + publishErrorOutbound(self.bus_ptr, msg.channel, msg.chat_id, "Cancelled by user."); + publishTraceEvent(self.bus_ptr, msg.channel, msg.chat_id, "llm_cancelled", "user cancelled", "", std.time.milliTimestamp() - llm_start); + return; + } if (self.circuit_breaker) |cb| cb.recordFailure(); + publishTraceEvent(self.bus_ptr, msg.channel, msg.chat_id, "llm_error", @errorName(err), "", std.time.milliTimestamp() - llm_start); return err; }; + const llm_elapsed = std.time.milliTimestamp() - llm_start; + publishTraceEvent(self.bus_ptr, msg.channel, msg.chat_id, "llm_response", response.finish_reason, response.content orelse "", llm_elapsed); + if (response.error_status_code) |code| { const err_msg = try std.fmt.allocPrint(aa, "agent_loop: LLM error status {d}, kind {?s}", .{ code, response.error_kind }); eva_app.logErr(err_msg); @@ -457,12 +609,45 @@ pub const AgentLoop = struct { if (c.len > 0) { if (final_response) |old| self.allocator.free(old); final_response = try self.allocator.dupe(u8, c); + + // Detect repeated same-text responses (LLM stuck in tool loop) + if (prev_response) |pr| { + if (std.mem.eql(u8, c, pr)) { + same_response_count += 1; + if (same_response_count >= 1) { + const loop_log = try std.fmt.allocPrint(aa, "agent_loop: same text response repeated {d} times, breaking tool loop", .{same_response_count + 1}); + eva_app.logWarn(loop_log); + force_break_tool_loop = true; + } + } else { + same_response_count = 0; + } + } + // Save for next comparison + if (prev_response) |pr| { + self.allocator.free(pr); + prev_response = null; + } + prev_response = try self.allocator.dupe(u8, c); + } else if (response.tool_calls.len > 0 and self.tool_registry != null) { + // Empty content + tool calls. Allow ONE such turn (Gemini + // often emits tool calls without text on the first attempt). + // If it persists, break the silent loop. + silent_tool_count += 1; + if (silent_tool_count >= 2) { + const silent_log = try std.fmt.allocPrint(aa, "agent_loop: empty content with tool calls (x{d}), breaking silent loop", .{silent_tool_count}); + eva_app.logWarn(silent_log); + if (final_response == null) { + final_response = try self.allocator.dupe(u8, eva_mod.Messages.repeatedToolCall); + } + force_break_tool_loop = true; + } } } else { eva_app.logDebug("agent_loop: response.content is null"); } - const has_tool_calls = response.tool_calls.len > 0 and self.tool_registry != null; + const has_tool_calls = !force_break_tool_loop and response.tool_calls.len > 0 and self.tool_registry != null; const tool_calls_data: std.ArrayList(providers.ToolCall) = if (has_tool_calls) blk: { var list = std.ArrayList(providers.ToolCall).initCapacity(aa, response.tool_calls.len) catch unreachable; for (response.tool_calls) |tc| { @@ -505,13 +690,16 @@ pub const AgentLoop = struct { break; } } else { - const sig_dup = try aa.dupe(u8, sig); + const sig_dup = try self.allocator.dupe(u8, sig); tool_call_history.put(sig_dup, 1) catch {}; } const arg_log = try std.fmt.allocPrint(aa, "agent_loop: tool {s} args: {s}", .{tc.name, tc.arguments}); eva_app.logDebug(arg_log); + const tool_start = std.time.milliTimestamp(); + publishTraceEvent(self.bus_ptr, msg.channel, msg.chat_id, "tool_call", tc.name, tc.arguments, 0); + var scanner = std.json.Scanner.initCompleteInput(aa, tc.arguments); defer scanner.deinit(); @@ -561,18 +749,27 @@ pub const AgentLoop = struct { eva_app.logErr(err_log); const err_result = try std.fmt.allocPrint(aa, "{{\"error\":\"tool execution failed: {any}\"}}", .{err}); try sess.addMessageWithToolCallId("tool", err_result, tc.id); + publishTraceEvent(self.bus_ptr, msg.channel, msg.chat_id, "tool_error", tc.name, @errorName(err), std.time.milliTimestamp() - tool_start); continue; }; + publishTraceEvent(self.bus_ptr, msg.channel, msg.chat_id, "tool_result", tc.name, "", std.time.milliTimestamp() - tool_start); + const result_log = try std.fmt.allocPrint(aa, "agent_loop: tool {s} result len={d}", .{ tc.name, result.len }); eva_app.logDebug(result_log); - if (result.len > 0 and result[0] == '{') { - if (std.mem.indexOf(u8, result, "\"error\"")) |_| { - const err_preview = if (result.len > 200) result[0..200] else result; - const err_preview_log = try std.fmt.allocPrint(aa, "agent_loop: tool {s} returned error: {s}", .{ tc.name, err_preview }); - eva_app.logWarn(err_preview_log); + if (std.mem.indexOfScalar(u8, result, '(')) |open_paren| { + const after_paren = result[open_paren + 1 ..]; + if (std.mem.indexOf(u8, after_paren, "ms)")) |ms_end| { + const num_slice = after_paren[0..ms_end]; + const elapsed_log = try std.fmt.allocPrint(aa, "agent_loop: tool {s} fetched in {s}ms", .{ tc.name, num_slice }); + eva_app.logInfo(elapsed_log); } } + if (std.mem.startsWith(u8, result, "Error:")) { + const err_preview = if (result.len > 200) result[0..200] else result; + const err_preview_log = try std.fmt.allocPrint(aa, "agent_loop: tool {s} returned error: {s}", .{ tc.name, err_preview }); + eva_app.logWarn(err_preview_log); + } try sess.addMessageWithToolCallId("tool", result, tc.id); @@ -626,7 +823,7 @@ pub const AgentLoop = struct { if (final_response) |resp| { self.state = .compact; - const save_log = try std.fmt.allocPrint(aa, "agent_loop: appending assistant to session key={s}", .{session_key}); + const save_log = try std.fmt.allocPrint(std.heap.page_allocator, "agent_loop: appending assistant to session key={s}", .{session_key}); eva_app.logDebug(save_log); try sess.addMessage("assistant", resp); sess.prune(self.session_max_messages); @@ -634,34 +831,38 @@ pub const AgentLoop = struct { if (self.memory_store) |store| { const turn_count = sess.messages.items.len / 2; if (turn_count > 0 and turn_count % self.consolidate_after_turns == 0) { - const consolidate_log = try std.fmt.allocPrint(aa, "agent_loop: triggering consolidation after {d} turns", .{turn_count}); + const consolidate_log = try std.fmt.allocPrint(std.heap.page_allocator, "agent_loop: triggering consolidation after {d} turns", .{turn_count}); eva_app.logInfo(consolidate_log); const history_for_consolidation = sess.getHistory(50); store.consolidate(self.provider, session_key, history_for_consolidation) catch |err| { - const err_log = try std.fmt.allocPrint(aa, "agent_loop: consolidation error: {any}", .{err}); + const err_log = try std.fmt.allocPrint(std.heap.page_allocator, "agent_loop: consolidation error: {any}", .{err}); eva_app.logErr(err_log); }; } } self.state = .respond; - const pub_log = try std.fmt.allocPrint(aa, "agent_loop: publishing outbound channel={s} chat_id={s}", .{msg.channel, msg.chat_id}); + const pub_log = try std.fmt.allocPrint(std.heap.page_allocator, "agent_loop: publishing outbound channel={s} chat_id={s}", .{msg.channel, msg.chat_id}); eva_app.logDebug(pub_log); - const done_channel_dup = try std.heap.page_allocator.dupe(u8, msg.channel); - errdefer std.heap.page_allocator.free(done_channel_dup); - const done_chat_id_dup = try std.heap.page_allocator.dupe(u8, msg.chat_id); - errdefer std.heap.page_allocator.free(done_chat_id_dup); - self.bus_ptr.publishStreaming(.{ - .channel = done_channel_dup, - .chat_id = done_chat_id_dup, - .delta = "", - .is_done = true, - }) catch { - std.heap.page_allocator.free(done_channel_dup); - std.heap.page_allocator.free(done_chat_id_dup); - }; + // SSE `done=true` is suppressed for WebSocket — the outbound + // message below carries the complete response instead. + if (!std.mem.eql(u8, msg.channel, "websocket")) { + const done_channel_dup = try std.heap.page_allocator.dupe(u8, msg.channel); + errdefer std.heap.page_allocator.free(done_channel_dup); + const done_chat_id_dup = try std.heap.page_allocator.dupe(u8, msg.chat_id); + errdefer std.heap.page_allocator.free(done_chat_id_dup); + self.bus_ptr.publishStreaming(.{ + .channel = done_channel_dup, + .chat_id = done_chat_id_dup, + .delta = "", + .is_done = true, + }) catch { + std.heap.page_allocator.free(done_channel_dup); + std.heap.page_allocator.free(done_chat_id_dup); + }; + } const outbound_channel = try std.heap.page_allocator.dupe(u8, msg.channel); errdefer std.heap.page_allocator.free(outbound_channel); diff --git a/src/bus.zig b/src/bus.zig index 003eb20..11e6653 100644 --- a/src/bus.zig +++ b/src/bus.zig @@ -16,6 +16,22 @@ pub const InboundMessage = struct { chat_id: []const u8, content: []const u8, timestamp: i64, + /// Optional per-turn overrides set by the client. Any null field + /// means "use the configured default" (from EVA_MODEL, EVA_API_BASE, + /// etc.). The agent loop reads these in priority order: + /// 1. override (if set on this message) + /// 2. AppConfig value + /// 3. provider default + /// Strings (system_prompt) are page_allocator-duplicated by the WS + /// handler because they outlive the JSON parse. + overrides: ?Overrides = null, + + pub const Overrides = struct { + temperature: ?f32 = null, + top_p: ?f32 = null, + max_tokens: ?u32 = null, + system_prompt: ?[]const u8 = null, + }; pub fn sessionKey(self: *const InboundMessage, buf: *[64]u8) []const u8 { return std.fmt.bufPrint(buf, "{s}:{s}", .{ self.channel, self.chat_id }) catch unreachable; @@ -25,6 +41,9 @@ pub const InboundMessage = struct { allocator.free(self.sender_id); allocator.free(self.chat_id); allocator.free(self.content); + if (self.overrides) |ov| { + if (ov.system_prompt) |sp| allocator.free(sp); + } } }; @@ -41,6 +60,27 @@ pub const StreamingMessage = struct { } }; +/// Trace events emitted by the agent loop. The frontend renders these +/// as a step-by-step panel (e.g. "llm_call 1.2s", "tool web_fetch 0.3s", +/// "compact 5 turns"). All string fields are page_allocator-duplicated +/// by the producer because the message outlives the caller's arena. +pub const TraceMessage = struct { + channel: []const u8, + chat_id: []const u8, + step: []const u8, + label: []const u8, + detail: []const u8 = "", + elapsed_ms: i64 = 0, + + pub fn deinit(self: *const TraceMessage, allocator: std.mem.Allocator) void { + allocator.free(self.channel); + allocator.free(self.chat_id); + allocator.free(self.step); + allocator.free(self.label); + if (self.detail.len > 0) allocator.free(self.detail); + } +}; + pub const OutboundMessage = struct { channel: []const u8, chat_id: []const u8, @@ -188,6 +228,52 @@ const StreamingQueue = struct { } }; +const TraceQueue = struct { + items: std.ArrayList(TraceMessage), + read_idx: usize, + + fn init(allocator: std.mem.Allocator) TraceQueue { + return .{ + .items = std.ArrayList(TraceMessage).initCapacity(allocator, 64) catch unreachable, + .read_idx = 0, + }; + } + + fn deinit(self: *TraceQueue, allocator: std.mem.Allocator) void { + self.items.deinit(allocator); + } + + fn drain(self: *TraceQueue, allocator: std.mem.Allocator) void { + for (self.items.items[self.read_idx..]) |item| item.deinit(allocator); + self.read_idx = self.items.items.len; + } + + fn push(self: *TraceQueue, allocator: std.mem.Allocator, item: TraceMessage) !void { + try self.items.append(allocator, item); + } + + fn pop(self: *TraceQueue) ?TraceMessage { + if (self.read_idx >= self.items.items.len) return null; + const item = self.items.items[self.read_idx]; + self.read_idx += 1; + return item; + } + + fn isEmpty(self: *const TraceQueue) bool { + return self.read_idx >= self.items.items.len; + } + + fn compact(self: *TraceQueue, allocator: std.mem.Allocator) void { + if (self.read_idx == 0) return; + const remaining = self.items.items[self.read_idx..]; + var new_list = std.ArrayList(TraceMessage).initCapacity(allocator, remaining.len) catch return; + new_list.appendSlice(allocator, remaining) catch return; + self.items.deinit(allocator); + self.items = new_list; + self.read_idx = 0; + } +}; + const OutboundQueue = struct { items: std.ArrayList(OutboundMessage), read_idx: usize, @@ -244,6 +330,7 @@ pub const MessageBus = struct { inbound: InboundQueue, streaming: StreamingQueue, outbound: OutboundQueue, + trace: TraceQueue, mutex: std.Thread.Mutex, cond: std.Thread.Condition, running: bool, @@ -254,6 +341,7 @@ pub const MessageBus = struct { .inbound = InboundQueue.init(allocator), .streaming = StreamingQueue.init(allocator), .outbound = OutboundQueue.init(allocator), + .trace = TraceQueue.init(allocator), .mutex = std.Thread.Mutex{}, .cond = std.Thread.Condition{}, .running = false, @@ -264,9 +352,11 @@ pub const MessageBus = struct { self.inbound.drain(self.allocator); self.streaming.drain(self.allocator); self.outbound.drain(self.allocator); + self.trace.drain(self.allocator); self.inbound.deinit(self.allocator); self.streaming.deinit(self.allocator); self.outbound.deinit(self.allocator); + self.trace.deinit(self.allocator); } pub fn start(self: *MessageBus) void { @@ -343,12 +433,33 @@ pub const MessageBus = struct { return self.streaming.pop(); } + pub fn publishTrace(self: *MessageBus, msg: TraceMessage) !void { + self.mutex.lock(); + defer self.mutex.unlock(); + try self.trace.push(self.allocator, msg); + self.cond.broadcast(); + } + + pub fn consumeTrace(self: *MessageBus) ?TraceMessage { + self.mutex.lock(); + defer self.mutex.unlock(); + + while (self.trace.isEmpty()) { + if (!self.running) return null; + self.trace.compact(self.allocator); + self.cond.wait(&self.mutex); + } + + return self.trace.pop(); + } + /// Snapshot of current queue depths (inbound / outbound / streaming). /// Caller-owned struct so no allocator interaction needed. pub const QueueDepth = struct { inbound: usize, outbound: usize, streaming: usize, + trace: usize, }; pub fn queueDepth(self: *const MessageBus) QueueDepth { @@ -356,6 +467,7 @@ pub const MessageBus = struct { .inbound = self.inbound.items.items.len - self.inbound.read_idx, .outbound = self.outbound.items.items.len - self.outbound.read_idx, .streaming = self.streaming.items.items.len - self.streaming.read_idx, + .trace = self.trace.items.items.len - self.trace.read_idx, }; } }; @@ -412,6 +524,7 @@ test "MessageBus.queueDepth reports all three queues" { try std.testing.expectEqual(@as(usize, 0), d0.inbound); try std.testing.expectEqual(@as(usize, 0), d0.outbound); try std.testing.expectEqual(@as(usize, 0), d0.streaming); + try std.testing.expectEqual(@as(usize, 0), d0.trace); try bus.publishInbound(.{ .channel = "test", @@ -422,14 +535,24 @@ test "MessageBus.queueDepth reports all three queues" { }); try bus.publishOutbound(.{ .channel = "test", .chat_id = "c", .content = "out" }); try bus.publishStreaming(.{ .channel = "test", .chat_id = "c", .delta = "d" }); + try bus.publishTrace(.{ + .channel = try allocator.dupe(u8, "test"), + .chat_id = try allocator.dupe(u8, "c"), + .step = try allocator.dupe(u8, "llm_call"), + .label = try allocator.dupe(u8, "x"), + .detail = try allocator.dupe(u8, ""), + .elapsed_ms = 0, + }); const d1 = bus.queueDepth(); try std.testing.expectEqual(@as(usize, 1), d1.inbound); try std.testing.expectEqual(@as(usize, 1), d1.outbound); try std.testing.expectEqual(@as(usize, 1), d1.streaming); + try std.testing.expectEqual(@as(usize, 1), d1.trace); bus.stop(); _ = bus.consumeInbound(); _ = bus.consumeOutbound(); _ = bus.consumeStreaming(); + if (bus.consumeTrace()) |t| t.deinit(allocator); } diff --git a/src/channels/websocket.zig b/src/channels/websocket.zig index 01c07dc..f258bc7 100644 --- a/src/channels/websocket.zig +++ b/src/channels/websocket.zig @@ -6,9 +6,13 @@ //! session count, circuit-breaker state). //! //! Outbound messages are dispatched via `dispatchOutbound` (full message -//! envelope) and `dispatchStreaming` (incremental delta updates for -//! streaming LLM responses). A per-instance `BufferPool` reduces -//! per-message allocator churn. +//! envelope), `dispatchStreaming` (incremental delta updates for +//! streaming LLM responses), and `dispatchTrace` (agent step events). +//! Every outbound payload is terminated with a `\n` newline so that +//! even if the underlying socket layer coalesces several writes into +//! one WS frame, the frontend can still split on `\n` and recover the +//! individual messages (NDJSON over WS). A per-instance `BufferPool` +//! reduces per-message allocator churn. const std = @import("std"); const zero = @import("zero"); @@ -18,6 +22,7 @@ const bus = @import("../bus.zig"); const eva_mod = @import("../eva.zig"); const session_mod = @import("../session.zig"); const cb_mod = @import("../circuit_breaker.zig"); +const agent_loop_mod = @import("../agent_loop.zig"); const WsConnection = struct { conn: *websocket.Conn, @@ -46,6 +51,23 @@ pub const WsRegistry = struct { fn add(self: *WsRegistry, conn: WsConnection) void { self.mutex.lock(); defer self.mutex.unlock(); + // One connection per chat_id. A reconnecting browser hands us + // a brand-new WebSocket before the old one's onClose has fired + // (the OS-level close handshake is async). If we appended both, + // dispatchOutbound would broadcast the same message to both, + // and the browser's still-attached old listener would push N + // copies of the assistant reply into the store. The new + // connection wins; the old one is told to close so its client + // listener drains and tears down promptly. + var i: usize = 0; + while (i < self.connections.items.len) { + if (std.mem.eql(u8, self.connections.items[i].chat_id, conn.chat_id)) { + const stale = self.connections.swapRemove(i); + stale.conn.close(.{}) catch {}; + } else { + i += 1; + } + } self.connections.append(self.allocator, conn) catch return; } @@ -86,6 +108,28 @@ pub const WsClient = struct { chat_id: []const u8, sender_id: []const u8, state: *WsState, + /// Per-connection dedup state. The first-turn race in the + /// frontend (parallel createNewChat calls all landing on the + /// last-generated chatId, then N `sendMessage` calls going out + /// from N onSubmit listeners) can still slip through the + /// frontend's 500 ms content dedup when the bug originates in + /// addEventListener accumulation (which can fire onSubmit far + /// more than 500 ms apart if the listeners keep stacking up + /// across re-renders). A second-line dedup here, keyed on + /// (content, chat_id), drops the burst server-side so the + /// agent loop only sees one copy. 500 ms is short enough that + /// legitimate rapid-fire sends of different content are + /// unaffected. + /// + /// NB: `last_inbound_content` is a private copy owned by the + /// WsClient (allocated via page_allocator.dupe on every + /// accept, freed on replace or onClose). The InboundMessage's + /// `content` field points to a different buffer that's owned + /// by the bus and freed by the agent loop — storing a pointer + /// to that buffer here would create a double-free when the + /// agent loop releases it. + last_inbound_content: ?[]const u8 = null, + last_inbound_at_ms: i64 = 0, pub fn init(conn: *websocket.Conn, ctx: *const WsContext) !WsClient { const chat_id = if (ctx.chat_id.len > 0) @@ -111,8 +155,9 @@ pub const WsClient = struct { return self.conn.write("{\"type\":\"connected\",\"chat_id\":\"ws:anonymous\"}"); } - pub fn onClose(self: *WsClient) void { + pub fn close(self: *WsClient) void { self.state.registry.remove(self.chat_id); + if (self.last_inbound_content) |prev| std.heap.page_allocator.free(prev); } pub fn clientMessage(self: *WsClient, data: []const u8) !void { @@ -124,6 +169,14 @@ pub const WsClient = struct { chat_id: ?[]const u8 = null, sender_id: ?[]const u8 = null, content: ?[]const u8 = null, + // Per-turn overrides. All optional; the agent loop falls + // back to AppConfig when null. system_prompt is duplicated + // off the parse allocator so it lives until the agent loop + // consumes the InboundMessage. + temperature: ?f32 = null, + top_p: ?f32 = null, + max_tokens: ?u32 = null, + system_prompt: ?[]const u8 = null, }; const parsed = std.json.parseFromTokenSource( @@ -144,12 +197,63 @@ pub const WsClient = struct { const content = std.heap.page_allocator.dupe(u8, parsed.value.content orelse "") catch return; errdefer std.heap.page_allocator.free(content); + // Per-connection dedup. See last_inbound_content comment. + // Make a private copy of `content` for the dedup buffer + // — the InboundMessage's content is owned by the bus and + // will be freed by the agent loop; we cannot keep a + // pointer to it. + const now_ms = std.time.milliTimestamp(); + if (self.last_inbound_content) |prev| { + if (std.mem.eql(u8, prev, content) and now_ms - self.last_inbound_at_ms < 500) { + std.heap.page_allocator.free(sender_id); + std.heap.page_allocator.free(chat_id); + std.heap.page_allocator.free(content); + return; + } + } + // Replace the dedup buffer. Free the previous one (we + // own it) and store a fresh copy of the new content. + if (self.last_inbound_content) |prev| std.heap.page_allocator.free(prev); + if (std.heap.page_allocator.dupe(u8, content)) |dup| { + self.last_inbound_content = dup; + } else |_| { + // If we can't alloc the dedup copy, just clear the + // dedup state — we'd rather lose dedup than crash. + self.last_inbound_content = null; + } + self.last_inbound_at_ms = now_ms; + + // Dupe the optional system_prompt into page_allocator + // because the JSON parse arena is freed when `parsed` + // goes out of scope, but the InboundMessage lives until + // the agent loop deinits it. The other overrides are + // values, not slices, so no duping needed. + const overrides: ?bus.InboundMessage.Overrides = blk: { + if (parsed.value.system_prompt == null and + parsed.value.temperature == null and + parsed.value.top_p == null and + parsed.value.max_tokens == null) + { + break :blk null; + } + var ov: bus.InboundMessage.Overrides = .{}; + if (parsed.value.system_prompt) |sp| { + ov.system_prompt = std.heap.page_allocator.dupe(u8, sp) catch return; + } + ov.temperature = parsed.value.temperature; + ov.top_p = parsed.value.top_p; + ov.max_tokens = parsed.value.max_tokens; + break :blk ov; + }; + errdefer if (overrides) |ov| if (ov.system_prompt) |sp| std.heap.page_allocator.free(sp); + const msg = bus.InboundMessage{ .channel = "websocket", .sender_id = sender_id, .chat_id = chat_id, .content = content, .timestamp = std.time.milliTimestamp(), + .overrides = overrides, }; self.state.bus_ptr.publishInbound(msg) catch { @@ -157,8 +261,23 @@ pub const WsClient = struct { std.heap.page_allocator.free(sender_id); std.heap.page_allocator.free(chat_id); std.heap.page_allocator.free(content); + if (overrides) |ov| if (ov.system_prompt) |sp| std.heap.page_allocator.free(sp); return; }; + return; + } + + // Cancel: flip the agent loop's atomic cancel flag so the + // in-flight chatStream returns error.Cancelled on the next + // token. We don't publish anything to the bus — the cancelled + // stream will publish its own "Cancelled by user." outbound + // and stop. + if (std.mem.eql(u8, parsed.value.type, "cancel")) { + if (self.state.agent_ptr) |agent| { + agent.requestCancel(); + _ = self.conn.write("{\"type\":\"cancelled\"}") catch {}; + } + return; } } }; @@ -179,9 +298,20 @@ pub const WsState = struct { port: u16, sessions: ?*session_mod.SessionManager = null, circuit_breaker: ?*cb_mod.CircuitBreaker = null, + /// Optional reference to the agent loop. Set by gateway.start() + /// so the WS "cancel" message type can flip the agent's cancel + /// flag mid-stream. Optional because the test harness constructs + /// WsState without an agent. + agent_ptr: ?*agent_loop_mod.AgentLoop = null, container: *zero.container, /// Process start time in ms (set in runWsServer). Used by /health. start_time_ms: i64 = 0, + /// When non-null, the HTTP server serves files from this directory + /// (e.g. Astro's `frontend/dist/`) for any non-API GET request. + /// Set via EVA_SERVE_FRONTEND=true. The frontend's own dev server + /// (port 4321) is the recommended dev path; this is for prod + /// single-binary hosting. + static_dir: ?[]const u8 = null, }; pub const WsHandler = struct { @@ -202,6 +332,23 @@ pub fn runWsServer(state: *WsState) !void { router.get("/ws", wsUpgrade, .{}); router.get("/health", wsHealth, .{}); + // Chat-management REST API. Routes require EVA_API_KEY bearer + // auth if the env var is set; otherwise open (dev-friendly). + const sessions_api = @import("../sessions_api.zig"); + router.get("/api/sessions", sessions_api.listSessions, .{}); + router.get("/api/config", sessions_api.getConfig, .{}); + router.get("/api/sessions/:id/messages", sessions_api.getSessionMessages, .{}); + router.patch("/api/sessions/:id", sessions_api.patchSession, .{}); + router.delete("/api/sessions/:id", sessions_api.deleteSession, .{}); + + // Catch-all for static files when EVA_SERVE_FRONTEND is set. + // Must be registered after all API routes so they take priority. + // The handler reads the root from `state.static_dir` at request + // time, so we can register a single function pointer. + if (state.static_dir != null) { + router.get("/*", wsStatic, .{}); + } + if (std.fmt.allocPrint(state.allocator, "WebSocket server listening", .{})) |info| { state.eva_app.logInfo(info); state.allocator.free(info); @@ -261,7 +408,7 @@ pub fn dispatchOutbound(state: *WsState, msg: bus.OutboundMessage, allocator: st writeJsonString(w, msg.chat_id) catch continue; w.writeAll("\",\"content\":\"") catch continue; writeJsonString(w, msg.content) catch continue; - w.writeAll("\"}") catch continue; + w.writeAll("\"}\n") catch continue; c.conn.write(buf.items) catch { eva_app.logErr("ws: send failed"); @@ -292,7 +439,7 @@ pub fn dispatchStreaming(state: *WsState, msg: bus.StreamingMessage, allocator: } else { w.writeAll("false") catch continue; } - w.writeAll("}") catch continue; + w.writeAll("}\n") catch continue; c.conn.write(buf.items) catch { eva_app.logErr("ws: delta send failed"); @@ -301,6 +448,38 @@ pub fn dispatchStreaming(state: *WsState, msg: bus.StreamingMessage, allocator: } } +pub fn dispatchTrace(state: *WsState, msg: bus.TraceMessage, allocator: std.mem.Allocator, eva_app: *eva_mod.App) void { + _ = allocator; + if (!std.mem.eql(u8, msg.channel, "websocket")) return; + + state.registry.mutex.lock(); + defer state.registry.mutex.unlock(); + + for (state.registry.connections.items) |c| { + const min_cap = msg.step.len + msg.label.len + msg.detail.len + 192; + var buf = state.buf_pool.acquire(min_cap) catch continue; + defer state.buf_pool.release(buf); + + var w = buf.writer(state.allocator); + w.writeAll("{\"type\":\"trace\",\"chat_id\":\"") catch continue; + writeJsonString(w, msg.chat_id) catch continue; + w.writeAll("\",\"step\":\"") catch continue; + writeJsonString(w, msg.step) catch continue; + w.writeAll("\",\"label\":\"") catch continue; + writeJsonString(w, msg.label) catch continue; + w.writeAll("\",\"detail\":\"") catch continue; + writeJsonString(w, msg.detail) catch continue; + w.writeAll("\",\"elapsed_ms\":") catch continue; + var ib: [24]u8 = undefined; + w.writeAll(std.fmt.bufPrint(ib[0..], "{d}", .{msg.elapsed_ms}) catch "") catch continue; + w.writeAll("}\n") catch continue; + + c.conn.write(buf.items) catch { + eva_app.logErr("ws: trace send failed"); + }; + } +} + pub const BufferPool = struct { // Free-list of pre-allocated ArrayLists, keyed by capacity bucket. // Used by dispatchOutbound / dispatchStreaming to avoid per-message @@ -388,10 +567,93 @@ fn writeJsonString(writer: anytype, s: []const u8) !void { fn wsUpgrade(handler: WsHandler, req: *httpz.Request, res: *httpz.Response) !void { const ctx = WsContext{ .state = handler.state }; - if (try httpz.upgradeWebsocket(WsClient, req, res, &ctx) == false) { - res.status = 500; - res.body = "invalid websocket"; + // upgradeWebsocket writes the 101 response itself. If the client + // closes mid-handshake (Vite's dev WS proxy is known to do this), + // the underlying write fails with error.NotOpenForWriting. We must + // NOT propagate that error to httpz — its catch-all path then calls + // res.write() a second time on the now-closed FD, which hits + // posix.sendmsg's `.BADF => unreachable` and panics the worker + // thread. Returning normally (no res.status/body) leaves the + // connection in whatever state the kernel left it; the worker will + // see the close and tear down the FD without crashing. + if (httpz.upgradeWebsocket(WsClient, req, res, &ctx)) |_| { + // success + } else |_| { + // upgrade failed mid-handshake; do not write a 500 response + } +} + +/// Catch-all GET handler for static files (Astro's `frontend/dist/`). +/// Reads the root from `handler.state.static_dir` (set when +/// `EVA_SERVE_FRONTEND=true`). Path-traversal protected: rejects any +/// path containing `..`. Falls back to `index.html` for SPA-style +/// routes so the frontend router can take over. +fn wsStatic(handler: WsHandler, req: *httpz.Request, res: *httpz.Response) !void { + const root = handler.state.static_dir orelse { + res.status = 404; + res.body = "not found"; + return; + }; + + const path = req.url.path; + if (std.mem.indexOf(u8, path, "..") != null or std.mem.startsWith(u8, path, "//")) { + res.status = 400; + res.body = "bad path"; + return; } + var rel = if (path.len > 0 and path[0] == '/') path[1..] else path; + if (rel.len == 0) rel = "index.html"; + + const alloc = handler.state.allocator; + const full_path = try std.fs.path.join(alloc, &[_][]const u8{ root, rel }); + defer alloc.free(full_path); + + const file = std.fs.cwd().openFile(full_path, .{}) catch |err| { + if (err == error.FileNotFound) { + const index_path = try std.fs.path.join(alloc, &[_][]const u8{ root, "index.html" }); + defer alloc.free(index_path); + const idx = std.fs.cwd().openFile(index_path, .{}) catch { + res.status = 404; + res.body = "not found"; + return; + }; + defer idx.close(); + const stat = try idx.stat(); + const buf = try alloc.alloc(u8, stat.size); + defer alloc.free(buf); + _ = try idx.readAll(buf); + res.status = 200; + res.content_type = .HTML; + res.body = buf; + return; + } + res.status = 500; + res.body = "read error"; + return; + }; + defer file.close(); + + const stat = try file.stat(); + const buf = try alloc.alloc(u8, stat.size); + defer alloc.free(buf); + _ = try file.readAll(buf); + + res.status = 200; + res.content_type = contentTypeFor(rel); + res.body = buf; +} + +fn contentTypeFor(path: []const u8) httpz.ContentType { + if (std.mem.endsWith(u8, path, ".html")) return .HTML; + if (std.mem.endsWith(u8, path, ".js") or std.mem.endsWith(u8, path, ".mjs")) return .JS; + if (std.mem.endsWith(u8, path, ".css")) return .CSS; + if (std.mem.endsWith(u8, path, ".json")) return .JSON; + if (std.mem.endsWith(u8, path, ".svg")) return .SVG; + if (std.mem.endsWith(u8, path, ".woff") or std.mem.endsWith(u8, path, ".woff2")) return .WOFF; + if (std.mem.endsWith(u8, path, ".png")) return .PNG; + if (std.mem.endsWith(u8, path, ".jpg") or std.mem.endsWith(u8, path, ".jpeg")) return .JPG; + if (std.mem.endsWith(u8, path, ".ico")) return .ICO; + return .BINARY; } fn wsHealth(handler: WsHandler, _: *httpz.Request, res: *httpz.Response) !void { diff --git a/src/context_builder.zig b/src/context_builder.zig index 143d793..3e50e4c 100644 --- a/src/context_builder.zig +++ b/src/context_builder.zig @@ -39,25 +39,13 @@ pub const ContextBuilder = struct { self: *const ContextBuilder, allocator: std.mem.Allocator, history: []const providers.Message, - tool_defs: ?[]const providers.ToolDefinition, + system_prompt_override: ?[]const u8, ) !std.ArrayList(providers.Message) { var system_prompt = std.ArrayList(u8).initCapacity(allocator, 1024) catch unreachable; defer system_prompt.deinit(allocator); var writer = system_prompt.writer(allocator); - try writer.writeAll(self.system_prompt); - - if (tool_defs) |tools| { - if (tools.len > 0) { - try writer.writeAll("\n\n## Available Tools\nYou have access to the following tools. When the user's request matches a tool's capability, use the tool rather than answering directly.\n"); - for (tools) |t| { - try writer.writeAll("\n- **"); - try writer.writeAll(t.name); - try writer.writeAll("**: "); - try writer.writeAll(t.description); - } - } - } + try writer.writeAll(system_prompt_override orelse self.system_prompt); if (self.memory_store) |store| { const consolidated = store.getMemories(allocator, null, &[_][]const u8{ "consolidated" }, 20) catch null; @@ -127,7 +115,7 @@ pub const ContextBuilder = struct { messages: *std.ArrayList(providers.Message), allocator: std.mem.Allocator, history: []const providers.Message, - tool_defs: ?[]const providers.ToolDefinition, + system_prompt_override: ?[]const u8, ) !void { messages.clearRetainingCapacity(); @@ -135,19 +123,7 @@ pub const ContextBuilder = struct { defer system_prompt.deinit(allocator); var writer = system_prompt.writer(allocator); - try writer.writeAll(self.system_prompt); - - if (tool_defs) |tools| { - if (tools.len > 0) { - try writer.writeAll("\n\n## Available Tools\nYou have access to the following tools. When the user's request matches a tool's capability, use the tool rather than answering directly.\n"); - for (tools) |t| { - try writer.writeAll("\n- **"); - try writer.writeAll(t.name); - try writer.writeAll("**: "); - try writer.writeAll(t.description); - } - } - } + try writer.writeAll(system_prompt_override orelse self.system_prompt); if (self.memory_store) |store| { const consolidated = store.getMemories(allocator, null, &[_][]const u8{ "consolidated" }, 20) catch null; diff --git a/src/eva.zig b/src/eva.zig index 49131af..9377688 100644 --- a/src/eva.zig +++ b/src/eva.zig @@ -88,7 +88,7 @@ pub const AppConfig = struct { const system_prompt = zcfg.getOrDefault( "EVA_SYSTEM_PROMPT", - "You are a helpful AI assistant. Be concise and accurate.Avoid all emojis in response.", + "You are a helpful AI assistant. Be concise and accurate. When you call a tool, ALWAYS include text content — never return an empty text response. When you receive a tool result, immediately provide a final answer using the data; do not call additional tools. Avoid all emojis in response.", ); const max_tokens_str = zcfg.getOrDefault("EVA_MAX_TOKENS", "8192"); @@ -97,8 +97,8 @@ pub const AppConfig = struct { const context_window_str = zcfg.getOrDefault("EVA_CONTEXT_WINDOW", "65536"); const context_window_tokens = std.fmt.parseInt(u32, context_window_str, 10) catch 65536; - const max_iter_str = zcfg.getOrDefault("EVA_MAX_TOOL_ITERATIONS", "15"); - const max_tool_iterations = std.fmt.parseInt(u32, max_iter_str, 10) catch 15; + const max_iter_str = zcfg.getOrDefault("EVA_MAX_TOOL_ITERATIONS", "5"); + const max_tool_iterations = std.fmt.parseInt(u32, max_iter_str, 10) catch 5; const cb_threshold_str = zcfg.getOrDefault("EVA_CIRCUIT_BREAKER_THRESHOLD", "5"); const circuit_breaker_threshold = std.fmt.parseInt(u32, cb_threshold_str, 10) catch 5; @@ -217,6 +217,16 @@ pub const App = struct { const workspace_expanded = try expandTilde(allocator, workspace_raw); defer allocator.free(workspace_expanded); + // Ensure the workspace directory exists. Without this, first run + // fails inside `isWithinWorkspace` (the workspace realpath check + // returns error.FileNotFound → "path not found") and SQLite + // can't open the DB either. `makePath` is a no-op if the dir + // already exists, and creates all missing parents. + std.fs.cwd().makePath(workspace_expanded) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + const db_path = try std.fmt.allocPrint(allocator, "{s}/eva.db", .{workspace_expanded}); defer allocator.free(db_path); diff --git a/src/gateway.zig b/src/gateway.zig index c8bd8f5..7511d1e 100644 --- a/src/gateway.zig +++ b/src/gateway.zig @@ -49,6 +49,7 @@ pub const Gateway = struct { tg_thread: ?std.Thread = null, outbound_thread: ?std.Thread = null, stream_thread: ?std.Thread = null, + trace_thread: ?std.Thread = null, dream_thread: ?std.Thread = null, zhs_thread: ?std.Thread = null, @@ -267,8 +268,17 @@ pub const Gateway = struct { .port = port, .sessions = self.sessions_ptr, .circuit_breaker = self.circuit_breaker_ptr, + .agent_ptr = self.agent_ptr, .container = self.eva_app.app.container, .start_time_ms = 0, + .static_dir = blk: { + // EVA_SERVE_FRONTEND=true enables catch-all static + // serving from frontend/dist/. Default path; can + // be overridden via EVA_FRONTEND_DIR. + const enabled = std.posix.getenv("EVA_SERVE_FRONTEND") orelse ""; + if (!std.mem.eql(u8, enabled, "true") and !std.mem.eql(u8, enabled, "1")) break :blk null; + break :blk self.allocator.dupe(u8, std.posix.getenv("EVA_FRONTEND_DIR") orelse "frontend/dist") catch null; + }, }; self.ws_state_ptr = state; self.ws_thread = try std.Thread.spawn(.{}, wsThread, .{state}); @@ -320,6 +330,13 @@ pub const Gateway = struct { self.ws_state_ptr, }); + self.trace_thread = try std.Thread.spawn(.{}, traceThread, .{ + self.allocator, + self.bus_ptr, + self.eva_app, + self.ws_state_ptr, + }); + self.dream_thread = try std.Thread.spawn(.{}, dreamThread, .{ self.allocator, self.memory_store_ptr, @@ -439,6 +456,25 @@ pub const Gateway = struct { } } + fn traceThread( + allocator: std.mem.Allocator, + bus_ptr: *bus.MessageBus, + eva_app: *eva_mod.App, + ws_state_ptr: ?*ws_channel.WsState, + ) void { + while (true) { + const msg = bus_ptr.consumeTrace() orelse break; + + if (std.mem.eql(u8, msg.channel, "websocket")) { + if (ws_state_ptr) |state| { + ws_channel.dispatchTrace(state, msg, allocator, eva_app); + } + } + + msg.deinit(std.heap.page_allocator); + } + } + fn dreamThread( allocator: std.mem.Allocator, memory_store: *memory_mod.MemoryStore, diff --git a/src/http_client.zig b/src/http_client.zig index 1225568..fc257e1 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -18,7 +18,7 @@ pub const SSEEvent = struct { event: ?[]const u8 = null, data: []const u8 = "", - pub fn deinit(self: *SSEEvent, allocator: std.mem.Allocator) void { + pub fn deinit(self: *const SSEEvent, allocator: std.mem.Allocator) void { if (self.id) |id| allocator.free(id); if (self.event) |event| allocator.free(event); allocator.free(self.data); diff --git a/src/main.zig b/src/main.zig index e26890a..339294f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -8,6 +8,8 @@ const mig002 = @import("migrations/migration_002_create_messages.zig"); const mig003 = @import("migrations/migration_003_create_files.zig"); const mig004 = @import("migrations/migration_004_create_cronjobs.zig"); const mig005 = @import("migrations/migration_005_create_memories.zig"); +const mig006 = @import("migrations/migration_006_create_app_kv.zig"); +const mig007 = @import("migrations/migration_007_session_meta.zig"); pub const std_options: std.Options = .{ .logFn = zero.logger.custom, @@ -205,6 +207,14 @@ fn performZeroMigrations(app: *App) !void { std.fmt.comptimePrint("{d}", .{mig005.migrationNumber}), mig005._migrate, ); + try app.addMigration( + std.fmt.comptimePrint("{d}", .{mig006.migrationNumber}), + mig006._migrate, + ); + try app.addMigration( + std.fmt.comptimePrint("{d}", .{mig007.migrationNumber}), + mig007._migrate, + ); try app.runMigrations(); } diff --git a/src/memory.zig b/src/memory.zig index b7293c1..607f2d3 100644 --- a/src/memory.zig +++ b/src/memory.zig @@ -335,7 +335,7 @@ pub const MemoryStore = struct { defer self.allocator.free(user_msg.content); const msgs = [_]providers.Message{ system_msg, user_msg }; - const response = provider.chat(&msgs, null, "") catch |err| { + const response = provider.chat(&msgs, null, .{}) catch |err| { return err; }; defer response.deinit(self.allocator); @@ -384,7 +384,7 @@ pub const MemoryStore = struct { defer self.allocator.free(user_msg.content); const msgs = [_]providers.Message{ system_msg, user_msg }; - const response = provider.chat(&msgs, null, "") catch |err| { + const response = provider.chat(&msgs, null, .{}) catch |err| { return err; }; defer response.deinit(self.allocator); diff --git a/src/migrations/migration_007_session_meta.zig b/src/migrations/migration_007_session_meta.zig new file mode 100644 index 0000000..a3ee75e --- /dev/null +++ b/src/migrations/migration_007_session_meta.zig @@ -0,0 +1,43 @@ +const std = @import("std"); +const zero = @import("zero"); +const Context = zero.Context; + +pub const migrationNumber: i64 = 1771953342; + +pub fn run(c: *Context) !void { + // Chat-management metadata used by the Astro frontend's sidebar. + // All columns have safe defaults so the ALTER TABLE succeeds on + // pre-existing rows without a backfill pass. + const addTitleCol = + \\ALTER TABLE sessions ADD COLUMN title TEXT; + ; + _ = try c.SQLite.exec(addTitleCol, .{}); + + const addArchivedCol = + \\ALTER TABLE sessions ADD COLUMN archived INTEGER NOT NULL DEFAULT 0; + ; + _ = try c.SQLite.exec(addArchivedCol, .{}); + + const addFavoriteCol = + \\ALTER TABLE sessions ADD COLUMN favorite INTEGER NOT NULL DEFAULT 0; + ; + _ = try c.SQLite.exec(addFavoriteCol, .{}); + + const addSortOrderCol = + \\ALTER TABLE sessions ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0; + ; + _ = try c.SQLite.exec(addSortOrderCol, .{}); + + const createIndexes = + \\CREATE INDEX IF NOT EXISTS idx_sessions_archived ON sessions(archived); + \\CREATE INDEX IF NOT EXISTS idx_sessions_favorite ON sessions(favorite); + \\CREATE INDEX IF NOT EXISTS idx_sessions_sort_order ON sessions(sort_order); + \\CREATE INDEX IF NOT EXISTS idx_sessions_updated_at ON sessions(updated_at); + ; + _ = try c.SQLite.exec(createIndexes, .{}); +} + +pub const _migrate = &zero.migrate{ + .migrationNumber = migrationNumber, + .run = run, +}; diff --git a/src/providers/base.zig b/src/providers/base.zig index 97f2b44..7f558cf 100644 --- a/src/providers/base.zig +++ b/src/providers/base.zig @@ -73,13 +73,21 @@ pub const Message = struct { name: ?[]const u8 = null, }; +pub const Overrides = struct { + /// Per-turn model name. Empty string means "use provider default". + model: []const u8 = "", + temperature: ?f32 = null, + top_p: ?f32 = null, + max_tokens: ?u32 = null, +}; + pub const LLMProvider = struct { ptr: *anyopaque, vtable: *const VTable, pub const VTable = struct { - chat: *const fn (ptr: *anyopaque, messages: []const Message, tools: ?[]ToolDefinition, model: []const u8) anyerror!LLMResponse, - chatStream: *const fn (ptr: *anyopaque, messages: []const Message, tools: ?[]ToolDefinition, model: []const u8, onDelta: *const fn (*anyopaque, []const u8) void, onDeltaCtx: *anyopaque) anyerror!LLMResponse, + chat: *const fn (ptr: *anyopaque, messages: []const Message, tools: ?[]ToolDefinition, overrides: Overrides) anyerror!LLMResponse, + chatStream: *const fn (ptr: *anyopaque, messages: []const Message, tools: ?[]ToolDefinition, overrides: Overrides, onDelta: *const fn (*anyopaque, []const u8) void, onDeltaCtx: *anyopaque, cancel: ?*std.atomic.Value(bool)) anyerror!LLMResponse, deinit: *const fn (ptr: *anyopaque) void, }; @@ -94,32 +102,33 @@ pub const LLMProvider = struct { self.vtable.deinit(self.ptr); } - pub fn chat(self: *LLMProvider, messages: []const Message, tools: ?[]ToolDefinition, model: []const u8) !LLMResponse { - return self.vtable.chat(self.ptr, messages, tools, model); + pub fn chat(self: *LLMProvider, messages: []const Message, tools: ?[]ToolDefinition, overrides: Overrides) !LLMResponse { + return self.vtable.chat(self.ptr, messages, tools, overrides); } pub fn chatStream( self: *LLMProvider, messages: []const Message, tools: ?[]ToolDefinition, - model: []const u8, + overrides: Overrides, onDelta: *const fn (*anyopaque, []const u8) void, onDeltaCtx: *anyopaque, + cancel: ?*std.atomic.Value(bool), ) !LLMResponse { - return self.vtable.chatStream(self.ptr, messages, tools, model, onDelta, onDeltaCtx); + return self.vtable.chatStream(self.ptr, messages, tools, overrides, onDelta, onDeltaCtx, cancel); } pub fn chatWithRetry( self: *LLMProvider, messages: []const Message, tools: ?[]ToolDefinition, - model: []const u8, + overrides: Overrides, max_retries: u8, ) !LLMResponse { var retries: u8 = 0; const base_delay_ms: u64 = 1000; while (retries <= max_retries) : (retries += 1) { - const response = self.chat(messages, tools, model) catch |err| { + const response = self.chat(messages, tools, overrides) catch |err| { if (retries < max_retries) { const delay_ms = base_delay_ms << @intCast(retries); const jitter = std.crypto.random.intRangeLessThan(u64, 0, delay_ms / 2 + 1); @@ -133,3 +142,24 @@ pub const LLMProvider = struct { return error.MaxRetriesExceeded; } }; + +test "Overrides: zero value is empty" { + const o: Overrides = .{}; + try std.testing.expectEqualStrings("", o.model); + try std.testing.expect(o.temperature == null); + try std.testing.expect(o.top_p == null); + try std.testing.expect(o.max_tokens == null); +} + +test "Overrides: explicit fields round-trip" { + const o: Overrides = .{ + .model = "gpt-4", + .temperature = 0.7, + .top_p = 0.9, + .max_tokens = 1024, + }; + try std.testing.expectEqualStrings("gpt-4", o.model); + try std.testing.expectEqual(@as(f32, 0.7), o.temperature.?); + try std.testing.expectEqual(@as(f32, 0.9), o.top_p.?); + try std.testing.expectEqual(@as(u32, 1024), o.max_tokens.?); +} diff --git a/src/providers/openai_compat.zig b/src/providers/openai_compat.zig index 23138cc..ca63641 100644 --- a/src/providers/openai_compat.zig +++ b/src/providers/openai_compat.zig @@ -79,14 +79,15 @@ pub const OpenAICompatProvider = struct { self: *OpenAICompatProvider, messages: []const base.Message, tools: ?[]base.ToolDefinition, - model: []const u8, + overrides: base.Overrides, stream: bool, onDelta: ?*const fn (*anyopaque, []const u8) void, onDeltaCtx: ?*anyopaque, + cancel: ?*std.atomic.Value(bool), ) !base.LLMResponse { - const target_model = if (model.len > 0) model else self.default_model; + const target_model = if (overrides.model.len > 0) overrides.model else self.default_model; - const payload = try self.buildPayload(target_model, messages, tools, stream); + const payload = try self.buildPayload(target_model, messages, tools, stream, overrides); defer self.allocator.free(payload); const extra_headers = [_]std.http.Header{ @@ -94,7 +95,7 @@ pub const OpenAICompatProvider = struct { }; if (stream) { - return try self.streamChat(self.chat_url, &extra_headers, payload, onDelta.?, onDeltaCtx.?); + return try self.streamChat(self.chat_url, &extra_headers, payload, onDelta.?, onDeltaCtx.?, cancel); } return try self.nonStreamChat(self.chat_url, &extra_headers, payload); } @@ -237,6 +238,7 @@ pub const OpenAICompatProvider = struct { body: []const u8, onDelta: *const fn (*anyopaque, []const u8) void, onDeltaCtx: *anyopaque, + cancel: ?*std.atomic.Value(bool), ) !base.LLMResponse { var payload_buf = std.ArrayList(u8).initCapacity(self.allocator, body.len + 32) catch unreachable; defer payload_buf.deinit(self.allocator); @@ -272,6 +274,8 @@ pub const OpenAICompatProvider = struct { defer sr.deinit(); while (try sr.nextEvent()) |event| { + defer event.deinit(self.allocator); + if (cancel) |c| if (c.load(.acquire)) return error.Cancelled; if (std.mem.eql(u8, event.data, "[DONE]")) { break; } @@ -297,6 +301,8 @@ pub const OpenAICompatProvider = struct { defer sr.deinit(); while (try sr.nextEvent()) |event| { + defer event.deinit(self.allocator); + if (cancel) |c| if (c.load(.acquire)) return error.Cancelled; if (std.mem.eql(u8, event.data, "[DONE]")) { break; } @@ -353,6 +359,7 @@ pub const OpenAICompatProvider = struct { current_tool_args: *std.ArrayList(u8), ) !void { if (extractDeltaContent(allocator, data)) |delta| { + defer allocator.free(delta); if (delta.len > 0) { accumulated.appendSlice(allocator, delta) catch {}; onDelta(onDeltaCtx, delta); @@ -390,6 +397,7 @@ pub const OpenAICompatProvider = struct { } if (tc_delta.arguments) |args| { try current_tool_args.appendSlice(allocator, args); + allocator.free(args); } if (tc_delta.is_complete) { if (current_tool_id.*) |id| { @@ -649,16 +657,17 @@ pub const OpenAICompatProvider = struct { ptr: *anyopaque, messages: []const base.Message, tools: ?[]base.ToolDefinition, - model: []const u8, + overrides: base.Overrides, ) anyerror!base.LLMResponse { const self: *OpenAICompatProvider = @ptrCast(@alignCast(ptr)); return self.chatImpl( messages, tools, - model, + overrides, false, null, null, + null, ); } @@ -666,18 +675,20 @@ pub const OpenAICompatProvider = struct { ptr: *anyopaque, messages: []const base.Message, tools: ?[]base.ToolDefinition, - model: []const u8, + overrides: base.Overrides, onDelta: *const fn (*anyopaque, []const u8) void, onDeltaCtx: *anyopaque, + cancel: ?*std.atomic.Value(bool), ) anyerror!base.LLMResponse { const self: *OpenAICompatProvider = @ptrCast(@alignCast(ptr)); return self.chatImpl( messages, tools, - model, + overrides, true, onDelta, onDeltaCtx, + cancel, ); } @@ -746,6 +757,7 @@ pub const OpenAICompatProvider = struct { messages: []const base.Message, tools: ?[]base.ToolDefinition, stream: bool, + overrides: base.Overrides, ) ![]u8 { const Msg = struct { role: []const u8, @@ -840,6 +852,25 @@ pub const OpenAICompatProvider = struct { try pw.writeAll(",\"stream\":"); try pw.writeAll(if (stream) "true" else "false"); + + // Per-turn overrides. Only emit when set so provider defaults + // (e.g. sampling temperature 1.0) stay intact for callers that + // didn't specify. + if (overrides.temperature) |t| { + var b: [64]u8 = undefined; + const s = std.fmt.bufPrint(b[0..], ",\"temperature\":{d}", .{t}) catch ""; + try pw.writeAll(s); + } + if (overrides.top_p) |tp| { + var b: [64]u8 = undefined; + const s = std.fmt.bufPrint(b[0..], ",\"top_p\":{d}", .{tp}) catch ""; + try pw.writeAll(s); + } + if (overrides.max_tokens) |mt| { + var b: [64]u8 = undefined; + const s = std.fmt.bufPrint(b[0..], ",\"max_tokens\":{d}", .{mt}) catch ""; + try pw.writeAll(s); + } try pw.writeAll("}"); return payload_buf.toOwnedSlice(self.allocator); diff --git a/src/root.zig b/src/root.zig index 57c7f4f..c7a33c7 100644 --- a/src/root.zig +++ b/src/root.zig @@ -44,6 +44,7 @@ pub const channel_manager = @import("channel_manager.zig"); pub const http_client = @import("http_client.zig"); pub const gateway = @import("gateway.zig"); pub const scheduler = @import("scheduler.zig"); +pub const sessions_api = @import("sessions_api.zig"); pub const Gateway = gateway.Gateway; pub const Scheduler = scheduler.Scheduler; @@ -91,6 +92,7 @@ comptime { _ = http_client; _ = gateway; _ = scheduler; + _ = sessions_api; _ = providers.base; _ = providers.openai_compat; _ = providers.anthropic; diff --git a/src/session.zig b/src/session.zig index 9dc37f4..92026bc 100644 --- a/src/session.zig +++ b/src/session.zig @@ -20,6 +20,9 @@ pub const Session = struct { /// the least-recently-used session for eviction. last_access_at: i64, loaded: bool, + /// Timestamp (ms) of the most recent "user" role message added. + /// Used for duplicate detection across rapid WS reconnect. + user_last_ms: i64 = 0, pub const max_tool_content_len: usize = 2048; @@ -120,6 +123,9 @@ pub const Session = struct { ); self.updated_at = now; + if (std.mem.eql(u8, role, "user")) { + self.user_last_ms = now; + } try self.db.exec( "UPDATE sessions SET updated_at = ? WHERE key = ?", .{ now, self.key }, @@ -422,6 +428,146 @@ pub const SessionManager = struct { oldest_session.deinit(); self.allocator.destroy(oldest_session); } + + /// Snapshot of one session row, for the /api/sessions listing. + /// `key` is the full session key (e.g. `websocket:`). + pub const Meta = struct { + key: []const u8, + title: ?[]const u8, + created_at: i64, + updated_at: i64, + archived: i64, + favorite: i64, + sort_order: i64, + message_count: i64, + }; + + /// List all sessions with the chat-management metadata. Sorted: + /// favorites first (by sort_order), then the rest (by sort_order). + /// The caller owns the returned slice and the `key` strings inside + /// it; free with `freeMetaList`. + pub fn listAll(self: *SessionManager) ![]Meta { + const Q = + \\SELECT s.key, s.title, s.created_at, s.updated_at, + \\ s.archived, s.favorite, s.sort_order, + \\ (SELECT COUNT(*) FROM messages m WHERE m.session_key = s.key) AS message_count + \\FROM sessions s + \\ORDER BY s.favorite DESC, s.sort_order DESC, s.updated_at DESC + ; + return try self.db.queryRows(Meta, self.allocator, Q, .{}); + } + + /// Free a meta list returned by `listAll`. + pub fn freeMetaList(self: *SessionManager, metas: []Meta) void { + for (metas) |r| { + self.allocator.free(r.key); + if (r.title) |t| self.allocator.free(t); + } + self.allocator.free(metas); + } + + /// Remove a session from the in-memory cache without touching the DB. + /// Safe to call from the REST API handlers that manage their own SQL. + pub fn invalidateCache(self: *SessionManager, key: []const u8) void { + if (self.cache.fetchRemove(key)) |entry| { + self.allocator.free(entry.key); + entry.value.deinit(); + self.allocator.destroy(entry.value); + } + } + + /// Delete a session row, its messages, and any cached entry. The + /// `key` must be the full session key (e.g. `websocket:`), not + /// the bare chat id. Returns true if a row was deleted. + pub fn deleteByKey(self: *SessionManager, key: []const u8) !bool { + self.invalidateCache(key); + try self.db.exec("DELETE FROM messages WHERE session_key = ?", .{key}); + try self.db.exec("DELETE FROM sessions WHERE key = ?", .{key}); + return self.db.rowsAffected() > 0; + } + + /// Patch one or more metadata fields on a session. Any field left + /// null is left unchanged. Returns true if a row was updated. + /// + /// SQL placeholders are emitted in a fixed order (title, archived, + /// favorite, sort_order) and the matching `db.exec` tuple is + /// constructed at the same call site, so the order never drifts. + pub fn updateMeta( + self: *SessionManager, + key: []const u8, + title: ?[]const u8, + archived: ?bool, + favorite: ?bool, + sort_order: ?i64, + ) !bool { + if (title == null and archived == null and favorite == null and sort_order == null) { + return false; + } + + var sql: std.ArrayList(u8) = .initCapacity(self.allocator, 128) catch unreachable; + defer sql.deinit(self.allocator); + try sql.appendSlice(self.allocator, "UPDATE sessions SET "); + var first = true; + if (title != null) { + if (!first) try sql.append(self.allocator, ','); + try sql.appendSlice(self.allocator, "title = ?"); + first = false; + } + if (archived != null) { + if (!first) try sql.append(self.allocator, ','); + try sql.appendSlice(self.allocator, "archived = ?"); + first = false; + } + if (favorite != null) { + if (!first) try sql.append(self.allocator, ','); + try sql.appendSlice(self.allocator, "favorite = ?"); + first = false; + } + if (sort_order != null) { + if (!first) try sql.append(self.allocator, ','); + try sql.appendSlice(self.allocator, "sort_order = ?"); + first = false; + } + try sql.appendSlice(self.allocator, " WHERE key = ?"); + const sql_text = try sql.toOwnedSlice(self.allocator); + defer self.allocator.free(sql_text); + + // Hand-roll the 16 permutations so the call sites stay type-safe. + const title_v = title orelse ""; + const archived_v: i64 = if (archived orelse false) 1 else 0; + const favorite_v: i64 = if (favorite orelse false) 1 else 0; + const sort_order_v = sort_order orelse 0; + _ = switch (.{ title != null, archived != null, favorite != null, sort_order != null }) { + .{ true, false, false, false } => try self.db.exec(sql_text, .{ key, title_v }), + .{ false, true, false, false } => try self.db.exec(sql_text, .{ key, archived_v }), + .{ false, false, true, false } => try self.db.exec(sql_text, .{ key, favorite_v }), + .{ false, false, false, true } => try self.db.exec(sql_text, .{ key, sort_order_v }), + .{ true, true, false, false } => try self.db.exec(sql_text, .{ key, title_v, archived_v }), + .{ true, false, true, false } => try self.db.exec(sql_text, .{ key, title_v, favorite_v }), + .{ true, false, false, true } => try self.db.exec(sql_text, .{ key, title_v, sort_order_v }), + .{ false, true, true, false } => try self.db.exec(sql_text, .{ key, archived_v, favorite_v }), + .{ false, true, false, true } => try self.db.exec(sql_text, .{ key, archived_v, sort_order_v }), + .{ false, false, true, true } => try self.db.exec(sql_text, .{ key, favorite_v, sort_order_v }), + .{ true, true, true, false } => try self.db.exec(sql_text, .{ key, title_v, archived_v, favorite_v }), + .{ true, true, false, true } => try self.db.exec(sql_text, .{ key, title_v, archived_v, sort_order_v }), + .{ true, false, true, true } => try self.db.exec(sql_text, .{ key, title_v, favorite_v, sort_order_v }), + .{ false, true, true, true } => try self.db.exec(sql_text, .{ key, archived_v, favorite_v, sort_order_v }), + .{ true, true, true, true } => try self.db.exec(sql_text, .{ key, title_v, archived_v, favorite_v, sort_order_v }), + else => unreachable, + }; + return self.db.rowsAffected() > 0; + } + + /// Look up the bare chat_id (stripped of the channel prefix) for a + /// session by its full key. Returns null if the key is not in the + /// `websocket:` form. The caller owns the returned slice. + pub fn chatIdFromKey(allocator: std.mem.Allocator, key: []const u8) !?[]u8 { + const prefix = "websocket:"; + if (std.mem.startsWith(u8, key, prefix)) { + return try allocator.dupe(u8, key[prefix.len..]); + } + return null; + } }; // Tests for Session that don't require a real DB. DB is never touched by diff --git a/src/sessions_api.zig b/src/sessions_api.zig new file mode 100644 index 0000000..121f5dd --- /dev/null +++ b/src/sessions_api.zig @@ -0,0 +1,488 @@ +//! REST API for chat-session management. Mounted on the same httpz +//! server as `/ws` and `/health`. All routes require `Authorization: +//! Bearer $EVA_API_KEY` if `EVA_API_KEY` is set in the environment; +//! otherwise open (dev-friendly). +//! +//! Routes: +//! GET /api/sessions +//! GET /api/sessions/:id/messages +//! PATCH /api/sessions/:id body: {title?, archived?, favorite?, sort_order?} +//! DELETE /api/sessions/:id +//! GET /api/config model, context_window, etc. +//! GET /api/health +//! +//! The handlers read state via `handler.state` (the zero WebsocketHandler +//! state holds an `*App` whose `eva_config` carries the api_key, and a +//! `*zero.container` whose `SQLite` is the DB). + +const std = @import("std"); +const zero = @import("zero"); + +const WsHandler = @import("channels/websocket.zig").WsHandler; +const session = @import("session.zig"); + +const httpz = zero.httpz; + +fn fullKey(allocator: std.mem.Allocator, id: []const u8) ![]u8 { + return std.fmt.allocPrint(allocator, "websocket:{s}", .{id}); +} + +fn jsonError(res: *httpz.Response, status: u16, msg: []const u8) void { + res.status = status; + res.body = msg; + res.content_type = .JSON; +} + +fn requireAuth(handler: WsHandler, req: *httpz.Request, res: *httpz.Response) !void { + const want = handler.state.eva_app.eva_config.api_key; + if (want.len == 0) return; + + const auth = req.header("authorization") orelse { + jsonError(res, 401, "{\"error\":\"missing authorization header\"}"); + return error.Unauthorized; + }; + const prefix = "Bearer "; + if (!std.mem.startsWith(u8, auth, prefix) or !std.mem.eql(u8, auth[prefix.len..], want)) { + jsonError(res, 401, "{\"error\":\"invalid authorization\"}"); + return error.Unauthorized; + } +} + +/// `GET /api/sessions` — list all chat sessions. +pub fn listSessions(handler: WsHandler, req: *httpz.Request, res: *httpz.Response) !void { + try requireAuth(handler, req, res); + const db = handler.state.container.SQLite orelse { + jsonError(res, 500, "{\"error\":\"sqlite not available\"}"); + return; + }; + const alloc = handler.state.allocator; + + const Row = struct { + key: []const u8, + title: ?[]const u8, + created_at: i64, + updated_at: i64, + archived: i64, + favorite: i64, + sort_order: i64, + message_count: i64, + }; + const Q = + \\SELECT s.key, s.title, s.created_at, s.updated_at, + \\ s.archived, s.favorite, s.sort_order, + \\ (SELECT COUNT(*) FROM messages m WHERE m.session_key = s.key) AS message_count + \\FROM sessions s + \\WHERE s.key LIKE 'websocket:%' + \\ORDER BY s.favorite DESC, s.sort_order DESC, s.updated_at DESC + ; + const rows = db.queryRows(Row, alloc, Q, .{}) catch |err| { + jsonError(res, 500, "{\"error\":\"db query failed\"}"); + std.log.err("sessions_api.listSessions: {any}", .{err}); + return; + }; + defer { + for (rows) |r| { + alloc.free(r.key); + if (r.title) |t| alloc.free(t); + } + alloc.free(rows); + } + + var body: std.ArrayList(u8) = .{}; + defer body.deinit(alloc); + try body.appendSlice(alloc, "{\"sessions\":["); + const prefix = "websocket:"; + var intbuf: [32]u8 = undefined; + for (rows, 0..) |r, i| { + if (i > 0) try body.append(alloc, ','); + const id_slice = if (std.mem.startsWith(u8, r.key, prefix)) r.key[prefix.len..] else r.key; + try body.appendSlice(alloc, "{\"id\":\""); + try appendJsonString(&body, alloc, id_slice); + try body.appendSlice(alloc, "\",\"created_at\":"); + try body.appendSlice(alloc, std.fmt.bufPrint(intbuf[0..], "{d}", .{r.created_at}) catch ""); + try body.appendSlice(alloc, ",\"updated_at\":"); + try body.appendSlice(alloc, std.fmt.bufPrint(intbuf[0..], "{d}", .{r.updated_at}) catch ""); + try body.appendSlice(alloc, ",\"archived\":"); + try body.appendSlice(alloc, if (r.archived != 0) "true" else "false"); + try body.appendSlice(alloc, ",\"favorite\":"); + try body.appendSlice(alloc, if (r.favorite != 0) "true" else "false"); + try body.appendSlice(alloc, ",\"sort_order\":"); + try body.appendSlice(alloc, std.fmt.bufPrint(intbuf[0..], "{d}", .{r.sort_order}) catch ""); + try body.appendSlice(alloc, ",\"message_count\":"); + try body.appendSlice(alloc, std.fmt.bufPrint(intbuf[0..], "{d}", .{r.message_count}) catch ""); + try body.appendSlice(alloc, ",\"title\":"); + if (r.title) |t| { + try body.append(alloc, '"'); + try appendJsonString(&body, alloc, t); + try body.append(alloc, '"'); + } else { + try body.appendSlice(alloc, "null"); + } + try body.append(alloc, '}'); + } + try body.appendSlice(alloc, "]}"); + + res.status = 200; + res.content_type = .JSON; + res.body = try body.toOwnedSlice(alloc); +} + +/// `GET /api/sessions/:id/messages` — full history. +pub fn getSessionMessages(handler: WsHandler, req: *httpz.Request, res: *httpz.Response) !void { + try requireAuth(handler, req, res); + const id = req.param("id") orelse { + jsonError(res, 400, "{\"error\":\"missing id\"}"); + return; + }; + if (id.len == 0) { + jsonError(res, 400, "{\"error\":\"empty id\"}"); + return; + } + const db = handler.state.container.SQLite orelse { + jsonError(res, 500, "{\"error\":\"sqlite not available\"}"); + return; + }; + const alloc = handler.state.allocator; + const key = try fullKey(alloc, id); + defer alloc.free(key); + + const Row = struct { + role: []const u8, + content: []const u8, + tool_call_id: ?[]const u8, + created_at: i64, + }; + const Q = + \\SELECT role, content, tool_call_id, created_at + \\FROM messages + \\WHERE session_key = ? + \\ORDER BY id ASC + ; + const rows = db.queryRows(Row, alloc, Q, .{key}) catch |err| { + jsonError(res, 500, "{\"error\":\"db query failed\"}"); + std.log.err("sessions_api.getMessages: {any}", .{err}); + return; + }; + defer { + for (rows) |r| { + alloc.free(r.role); + alloc.free(r.content); + if (r.tool_call_id) |t| alloc.free(t); + } + alloc.free(rows); + } + + var body: std.ArrayList(u8) = .{}; + defer body.deinit(alloc); + try body.appendSlice(alloc, "{\"messages\":["); + var intbuf: [32]u8 = undefined; + for (rows, 0..) |r, i| { + if (i > 0) try body.append(alloc, ','); + try body.appendSlice(alloc, "{\"role\":\""); + try appendJsonString(&body, alloc, r.role); + try body.appendSlice(alloc, "\",\"content\":\""); + try appendJsonString(&body, alloc, r.content); + try body.appendSlice(alloc, "\",\"created_at\":"); + try body.appendSlice(alloc, std.fmt.bufPrint(intbuf[0..], "{d}", .{r.created_at}) catch ""); + if (r.tool_call_id) |t| { + try body.appendSlice(alloc, ",\"tool_call_id\":\""); + try appendJsonString(&body, alloc, t); + try body.append(alloc, '"'); + } + try body.append(alloc, '}'); + } + try body.appendSlice(alloc, "]}"); + + res.status = 200; + res.content_type = .JSON; + res.body = try body.toOwnedSlice(alloc); +} + +/// `PATCH /api/sessions/:id` — update one or more metadata fields. +pub fn patchSession(handler: WsHandler, req: *httpz.Request, res: *httpz.Response) !void { + try requireAuth(handler, req, res); + const id = req.param("id") orelse { + jsonError(res, 400, "{\"error\":\"missing id\"}"); + return; + }; + if (id.len == 0) { + jsonError(res, 400, "{\"error\":\"empty id\"}"); + return; + } + const db = handler.state.container.SQLite orelse { + jsonError(res, 500, "{\"error\":\"sqlite not available\"}"); + return; + }; + const alloc = handler.state.allocator; + const key = try fullKey(alloc, id); + defer alloc.free(key); + + const raw = req.body() orelse { + jsonError(res, 400, "{\"error\":\"missing body\"}"); + return; + }; + + var parsed = std.json.parseFromSlice(std.json.Value, alloc, raw, .{ + .ignore_unknown_fields = true, + }) catch { + jsonError(res, 400, "{\"error\":\"invalid json\"}"); + return; + }; + defer parsed.deinit(); + + const obj = switch (parsed.value) { + .object => |o| o, + else => { + jsonError(res, 400, "{\"error\":\"body must be object\"}"); + return; + }, + }; + + var title_val: ?[]const u8 = null; + var archived_val: ?bool = null; + var favorite_val: ?bool = null; + var sort_order_val: ?i64 = null; + var has_any = false; + + if (obj.get("title")) |v| { + if (v != .null) { + title_val = v.string; + has_any = true; + } + } + if (obj.get("archived")) |v| { + if (v == .bool) { + archived_val = v.bool; + has_any = true; + } + } + if (obj.get("favorite")) |v| { + if (v == .bool) { + favorite_val = v.bool; + has_any = true; + } + } + if (obj.get("sort_order")) |v| { + if (v == .integer) { + sort_order_val = v.integer; + has_any = true; + } + } + + if (!has_any) { + jsonError(res, 400, "{\"error\":\"no updatable fields\"}"); + return; + } + + const t = title_val orelse ""; + const a: i64 = if (archived_val orelse false) 1 else 0; + const f: i64 = if (favorite_val orelse false) 1 else 0; + const s = sort_order_val orelse 0; + const t_present = title_val != null; + const a_present = archived_val != null; + const f_present = favorite_val != null; + const s_present = sort_order_val != null; + + // Each branch is a comptime string with its own exec call, so + // the static `comptime query` parameter is satisfied. Order of + // `?` placeholders is fixed: title, archived, favorite, sort_order. + if (t_present and !a_present and !f_present and !s_present) { + try db.exec("UPDATE sessions SET title = ? WHERE key = ?", .{ t, key }); + } else if (!t_present and a_present and !f_present and !s_present) { + try db.exec("UPDATE sessions SET archived = ? WHERE key = ?", .{ a, key }); + } else if (!t_present and !a_present and f_present and !s_present) { + try db.exec("UPDATE sessions SET favorite = ? WHERE key = ?", .{ f, key }); + } else if (!t_present and !a_present and !f_present and s_present) { + try db.exec("UPDATE sessions SET sort_order = ? WHERE key = ?", .{ s, key }); + } else if (t_present and a_present and !f_present and !s_present) { + try db.exec("UPDATE sessions SET title = ?, archived = ? WHERE key = ?", .{ t, a, key }); + } else if (t_present and !a_present and f_present and !s_present) { + try db.exec("UPDATE sessions SET title = ?, favorite = ? WHERE key = ?", .{ t, f, key }); + } else if (t_present and !a_present and !f_present and s_present) { + try db.exec("UPDATE sessions SET title = ?, sort_order = ? WHERE key = ?", .{ t, s, key }); + } else if (!t_present and a_present and f_present and !s_present) { + try db.exec("UPDATE sessions SET archived = ?, favorite = ? WHERE key = ?", .{ a, f, key }); + } else if (!t_present and a_present and !f_present and s_present) { + try db.exec("UPDATE sessions SET archived = ?, sort_order = ? WHERE key = ?", .{ a, s, key }); + } else if (!t_present and !a_present and f_present and s_present) { + try db.exec("UPDATE sessions SET favorite = ?, sort_order = ? WHERE key = ?", .{ f, s, key }); + } else if (t_present and a_present and f_present and !s_present) { + try db.exec("UPDATE sessions SET title = ?, archived = ?, favorite = ? WHERE key = ?", .{ t, a, f, key }); + } else if (t_present and a_present and !f_present and s_present) { + try db.exec("UPDATE sessions SET title = ?, archived = ?, sort_order = ? WHERE key = ?", .{ t, a, s, key }); + } else if (t_present and !a_present and f_present and s_present) { + try db.exec("UPDATE sessions SET title = ?, favorite = ?, sort_order = ? WHERE key = ?", .{ t, f, s, key }); + } else if (!t_present and a_present and f_present and s_present) { + try db.exec("UPDATE sessions SET archived = ?, favorite = ?, sort_order = ? WHERE key = ?", .{ a, f, s, key }); + } else if (t_present and a_present and f_present and s_present) { + try db.exec("UPDATE sessions SET title = ?, archived = ?, favorite = ?, sort_order = ? WHERE key = ?", .{ t, a, f, s, key }); + } + + if (db.rowsAffected() == 0) { + jsonError(res, 404, "{\"error\":\"session not found\"}"); + return; + } + + // Evict the session from the in-memory cache so the next + // access re-loads from DB (which is the canonical source for + // metadata). The cache holds the Session struct (messages); + // title/archived/favorite are metadata-only so eviction is + // mainly for consistency. + if (handler.state.sessions) |sessions_ptr| { + sessions_ptr.invalidateCache(key); + } + + _ = db.exec( + "UPDATE sessions SET updated_at = ? WHERE key = ?", + .{ std.time.milliTimestamp(), key }, + ) catch {}; + + res.status = 200; + res.content_type = .JSON; + res.body = "{\"status\":\"ok\"}"; +} + +/// `DELETE /api/sessions/:id` — cascade-delete a session. +pub fn deleteSession(handler: WsHandler, req: *httpz.Request, res: *httpz.Response) !void { + try requireAuth(handler, req, res); + const id = req.param("id") orelse { + jsonError(res, 400, "{\"error\":\"missing id\"}"); + return; + }; + if (id.len == 0) { + jsonError(res, 400, "{\"error\":\"empty id\"}"); + return; + } + const db = handler.state.container.SQLite orelse { + jsonError(res, 500, "{\"error\":\"sqlite not available\"}"); + return; + }; + const alloc = handler.state.allocator; + const key = try fullKey(alloc, id); + defer alloc.free(key); + + // Evict from cache first so getOrCreate won't return a stale + // in-memory session after the DB row is gone. + if (handler.state.sessions) |sessions_ptr| { + sessions_ptr.invalidateCache(key); + } + + _ = db.exec("DELETE FROM messages WHERE session_key = ?", .{key}) catch {}; + db.exec("DELETE FROM sessions WHERE key = ?", .{key}) catch { + jsonError(res, 500, "{\"error\":\"db delete failed\"}"); + return; + }; + if (db.rowsAffected() == 0) { + jsonError(res, 404, "{\"error\":\"session not found\"}"); + return; + } + res.status = 200; + res.content_type = .JSON; + res.body = "{\"status\":\"ok\"}"; +} + +/// `GET /api/config` — return model + context_window + max_tokens for +/// the input hint. Values come from the live `AppConfig`, set at +/// startup from EVA_* env vars. +pub fn getConfig(handler: WsHandler, req: *httpz.Request, res: *httpz.Response) !void { + try requireAuth(handler, req, res); + const cfg = &handler.state.eva_app.eva_config; + const alloc = handler.state.allocator; + + var body: std.ArrayList(u8) = .{}; + defer body.deinit(alloc); + try body.appendSlice(alloc, "{\"model\":\""); + try appendJsonString(&body, alloc, cfg.model); + try body.appendSlice(alloc, "\",\"context_window\":"); + var ib: [32]u8 = undefined; + try body.appendSlice(alloc, std.fmt.bufPrint(ib[0..], "{d}", .{cfg.context_window_tokens}) catch ""); + try body.appendSlice(alloc, ",\"max_tokens\":"); + try body.appendSlice(alloc, std.fmt.bufPrint(ib[0..], "{d}", .{cfg.max_tokens}) catch ""); + try body.appendSlice(alloc, ",\"max_tool_iterations\":"); + try body.appendSlice(alloc, std.fmt.bufPrint(ib[0..], "{d}", .{cfg.max_tool_iterations}) catch ""); + try body.appendSlice(alloc, ",\"api_key_required\":"); + try body.appendSlice(alloc, if (cfg.api_key.len > 0) "true" else "false"); + try body.append(alloc, '}'); + + res.status = 200; + res.content_type = .JSON; + res.body = try body.toOwnedSlice(alloc); +} + +/// `GET /api/health` — separate from zero's /health so the frontend +/// can poll without hitting the auth gate. Always returns 200. +pub fn health(_: WsHandler, _: *httpz.Request, res: *httpz.Response) !void { + res.status = 200; + res.content_type = .JSON; + res.body = "{\"status\":\"ok\"}"; +} + +/// Append `s` to `buf` with JSON-string escaping. Doesn't allocate; +/// safe for short strings (chat ids, titles). For tool results +/// already in JSON, callers should re-serialize rather than escape. +fn appendJsonString(buf: *std.ArrayList(u8), alloc: std.mem.Allocator, s: []const u8) !void { + for (s) |c| { + switch (c) { + '"' => try buf.appendSlice(alloc, "\\\""), + '\\' => try buf.appendSlice(alloc, "\\\\"), + '\n' => try buf.appendSlice(alloc, "\\n"), + '\r' => try buf.appendSlice(alloc, "\\r"), + '\t' => try buf.appendSlice(alloc, "\\t"), + 8 => try buf.appendSlice(alloc, "\\b"), + 12 => try buf.appendSlice(alloc, "\\f"), + 0...7, 11, 14...31 => { + var escbuf: [8]u8 = undefined; + const esc = std.fmt.bufPrint(escbuf[0..], "\\u{x:0>4}", .{c}) catch ""; + try buf.appendSlice(alloc, esc); + }, + else => try buf.append(alloc, c), + } + } +} + +test "appendJsonString: escapes control chars" { + const a = std.testing.allocator; + var buf: std.ArrayList(u8) = .{}; + defer buf.deinit(a); + try appendJsonString(&buf, a, "a\"b\nc"); + try std.testing.expectEqualStrings("a\\\"b\\nc", buf.items); +} + +test "appendJsonString: passes through ASCII" { + const a = std.testing.allocator; + var buf: std.ArrayList(u8) = .{}; + defer buf.deinit(a); + try appendJsonString(&buf, a, "hello world"); + try std.testing.expectEqualStrings("hello world", buf.items); +} + +test "fullKey: prepends websocket:" { + const a = std.testing.allocator; + const k = try fullKey(a, "abc-123"); + defer a.free(k); + try std.testing.expectEqualStrings("websocket:abc-123", k); +} + +test "appendJsonString: empty string" { + const a = std.testing.allocator; + var buf: std.ArrayList(u8) = .{}; + defer buf.deinit(a); + try appendJsonString(&buf, a, ""); + try std.testing.expectEqualStrings("", buf.items); +} + +test "appendJsonString: escapes backslash" { + const a = std.testing.allocator; + var buf: std.ArrayList(u8) = .{}; + defer buf.deinit(a); + try appendJsonString(&buf, a, "a\\b"); + try std.testing.expectEqualStrings("a\\\\b", buf.items); +} + +test "appendJsonString: escapes tab and CR" { + const a = std.testing.allocator; + var buf: std.ArrayList(u8) = .{}; + defer buf.deinit(a); + try appendJsonString(&buf, a, "a\tb\rc"); + try std.testing.expectEqualStrings("a\\tb\\rc", buf.items); +} diff --git a/src/tools/filesystem.zig b/src/tools/filesystem.zig index 682b534..730334e 100644 --- a/src/tools/filesystem.zig +++ b/src/tools/filesystem.zig @@ -32,28 +32,27 @@ fn normalizePath(allocator: std.mem.Allocator, path: []const u8) ![]const u8 { } fn isWithinWorkspace(allocator: std.mem.Allocator, workspace: []const u8, rel_path: []const u8) ![]const u8 { + // Workspace must exist. `eva.App.init` calls `makePath(workspace)` at + // startup, so this only fails if the user pointed EVA_WORKSPACE at a + // path that can't be created (perm denied, etc.). const ws_resolved = std.fs.cwd().realpathAlloc(allocator, workspace) catch |err| switch (err) { error.FileNotFound, error.AccessDenied, error.NotDir => return error.PathNotFound, else => return err, }; defer allocator.free(ws_resolved); + // Reject absolute paths outright — normalizePath would strip the + // leading `/` and silently turn `/etc/passwd` into `etc/passwd` + // relative to the workspace, hiding the attempt. + if (std.fs.path.isAbsolute(rel_path)) return error.PathTraversal; + const normalized = try normalizePath(allocator, rel_path); defer allocator.free(normalized); - const joined = try std.fs.path.join(allocator, &.{ ws_resolved, normalized }); - defer allocator.free(joined); - - const final_resolved = std.fs.cwd().realpathAlloc(allocator, joined) catch |err| switch (err) { - error.FileNotFound, error.AccessDenied, error.NotDir => return error.PathNotFound, - else => return err, - }; - defer allocator.free(final_resolved); - - if (!std.mem.startsWith(u8, final_resolved, ws_resolved)) return error.PathTraversal; - if (final_resolved.len > ws_resolved.len and final_resolved[ws_resolved.len] != '/') return error.PathTraversal; - - return allocator.dupe(u8, final_resolved); + // normalizePath collapses `..` by popping, so a malicious + // `../../etc` becomes `etc`. The joined path stays inside + // `ws_resolved`, which is what we want. + return std.fs.path.join(allocator, &.{ ws_resolved, normalized }); } fn jsonEscape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { @@ -135,6 +134,55 @@ test "list_dir: opens with iterate flag and walks entries" { try std.testing.expect(names.items.len >= 2); } +test "isWithinWorkspace: returns PathNotFound when workspace dir missing" { + // Regression: on first run, EVA_WORKSPACE may not exist. Without + // `makePath` in App.init, every write_file returned "path not + // found" and the LLM hit the tool-loop detector. + const a = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + // tmp.dir has not created a "missing" subdir; that's the workspace. + const missing_workspace = "missing_workspace_subdir"; + const result = isWithinWorkspace(a, missing_workspace, "file.txt"); + try std.testing.expectError(error.PathNotFound, result); +} + +test "isWithinWorkspace: succeeds for new files inside an existing workspace" { + // Regression: isWithinWorkspace used to call realpathAlloc on the + // *target file*, which made every first-time write_file return + // PathNotFound. The new version resolves only the workspace and + // returns the constructed target path. + const a = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + const subdir = "my_workspace"; + try tmp.dir.makeDir(subdir); + const subdir_path = try tmp.dir.realpathAlloc(a, subdir); + defer a.free(subdir_path); + + const resolved = try isWithinWorkspace(a, subdir_path, "new_file.txt"); + defer a.free(resolved); + + try std.testing.expect(std.mem.startsWith(u8, resolved, subdir_path)); + try std.testing.expect(std.mem.endsWith(u8, resolved, "new_file.txt")); +} + +test "isWithinWorkspace: rejects absolute paths" { + // /etc/passwd is not a relative path; the tool must reject it + // rather than silently stripping the leading slash. + const a = std.testing.allocator; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + const ws = try tmp.dir.realpathAlloc(a, "."); + defer a.free(ws); + + const result = isWithinWorkspace(a, ws, "/etc/passwd"); + try std.testing.expectError(error.PathTraversal, result); +} + pub const ReadFileTool = struct { allocator: std.mem.Allocator, workspace: []const u8, diff --git a/src/tools/web.zig b/src/tools/web.zig index a4a03a7..fc62878 100644 --- a/src/tools/web.zig +++ b/src/tools/web.zig @@ -36,29 +36,29 @@ pub const WebFetchTool = struct { fn vtableDescription(ptr: *anyopaque) []const u8 { _ = ptr; - return "Fetch the content of a web page as rendered markdown using Lightpanda browser. Use this to read web page content including JavaScript-rendered pages."; + return "Fetch the content of a web page as rendered markdown using Lightpanda browser. Use this tool ANY TIME the user asks for current or real-time information that may not be in your training data: weather, news, stock prices, sports scores, exchange rates, package tracking, flight status, today's date in a specific timezone, etc. Construct a direct URL to a source you trust and pass it as the `url` parameter. TIP: prefer compact response formats (e.g. wttr.in/?format=3 returns a one-line summary, NOT ?format=j1 which is 45KB of JSON). The smaller the response, the faster the synthesis. Supports JavaScript-rendered pages. Private/loopback URLs are rejected. The result is returned as plain text prefixed with the source URL and fetch timing — read the content directly to answer the user."; } fn vtableParameters(ptr: *anyopaque) []const u8 { _ = ptr; - return "{\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\",\"description\":\"The URL to fetch (http or https only)\"}},\"required\":[\"url\"]}"; + return "{\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\",\"description\":\"The full https:// or http:// URL to fetch. Prefer compact response formats when available (wttr.in/Location?format=3 is a one-line summary).\"}},\"required\":[\"url\"]}"; } fn vtableExecute(ptr: *anyopaque, allocator: std.mem.Allocator, params: std.json.Value, ctx: base.ToolContext) anyerror![]const u8 { _ = ctx; const self: *WebFetchTool = @ptrCast(@alignCast(ptr)); - const url_val = params.object.get("url") orelse return try allocator.dupe(u8, "{\"error\":\"missing url parameter\"}"); + const url_val = params.object.get("url") orelse return try allocator.dupe(u8, "Error: missing url parameter"); const url = url_val.string; - if (url.len == 0) return try allocator.dupe(u8, "{\"error\":\"url is empty\"}"); + if (url.len == 0) return try allocator.dupe(u8, "Error: url is empty"); if (!std.mem.startsWith(u8, url, "http://") and !std.mem.startsWith(u8, url, "https://")) { - return try allocator.dupe(u8, "{\"error\":\"only http:// and https:// URLs are allowed\"}"); + return try allocator.dupe(u8, "Error: only http:// and https:// URLs are allowed"); } if (isPrivateUrl(url)) { - return try allocator.dupe(u8, "{\"error\":\"fetching private/internal URLs is not allowed\"}"); + return try allocator.dupe(u8, "Error: fetching private/internal URLs is not allowed"); } const args = [_][]const u8{ @@ -86,15 +86,22 @@ pub const WebFetchTool = struct { var stderr_buf = std.ArrayList(u8).initCapacity(allocator, 0) catch unreachable; defer stderr_buf.deinit(allocator); + const start_ns = std.time.nanoTimestamp(); child.spawn() catch |err| { - return try std.fmt.allocPrint(allocator, "{{\"error\":\"failed to spawn lightpanda: {any}\"}}", .{err}); + return try std.fmt.allocPrint(allocator, "Error: failed to spawn lightpanda: {any}", .{err}); }; child.collectOutput(allocator, &stdout_buf, &stderr_buf, self.max_output_size + 4096) catch |err| { - return try std.fmt.allocPrint(allocator, "{{\"error\":\"failed to read lightpanda output: {any}\"}}", .{err}); + // collectOutput failed: kill the child to avoid zombies, then + // surface a timeout-ish error. The child may have already + // exited on its own; kill is best-effort. + _ = child.kill() catch {}; + const elapsed_ms: i64 = @intCast(@divTrunc(std.time.nanoTimestamp() - start_ns, std.time.ns_per_ms)); + return try std.fmt.allocPrint(allocator, "Error: lightpanda timed out or failed after {d}ms: {any}", .{ elapsed_ms, err }); }; const term = child.wait() catch |err| { - return try std.fmt.allocPrint(allocator, "{{\"error\":\"failed to wait for lightpanda: {any}\"}}", .{err}); + return try std.fmt.allocPrint(allocator, "Error: failed to wait for lightpanda: {any}", .{err}); }; + const elapsed_ms: i64 = @intCast(@divTrunc(std.time.nanoTimestamp() - start_ns, std.time.ns_per_ms)); const exit_code: i32 = switch (term) { .Exited => |code| code, @@ -113,16 +120,17 @@ pub const WebFetchTool = struct { defer allocator.free(escaped); const killed_by_signal = exit_code < 0; + const signal_note = if (killed_by_signal) " (killed by signal)" else ""; return try std.fmt.allocPrint( allocator, - "{{\"error\":\"lightpanda exited with code {d}\",\"killed_by_signal\":{any},\"stderr\":\"{s}\"}}", - .{ exit_code, killed_by_signal, escaped }, + "Error: lightpanda exited with code {d}{s} ({d}ms). stderr: {s}", + .{ exit_code, signal_note, elapsed_ms, escaped }, ); } var content = stdout_buf.items; if (content.len == 0) { - return try allocator.dupe(u8, "{\"error\":\"no content returned from page\"}"); + return try allocator.dupe(u8, "Error: no content returned from page"); } var truncated = false; @@ -135,10 +143,10 @@ pub const WebFetchTool = struct { defer allocator.free(escaped); if (truncated) { - return try std.fmt.allocPrint(allocator, "{{\"url\":\"{s}\",\"content\":\"{s}\",\"truncated\":true,\"note\":\"content truncated to {d} bytes\"}}", .{ url, escaped, self.max_output_size }); + return try std.fmt.allocPrint(allocator, "Fetched content from {s} ({d}ms, truncated to {d} bytes):\n\n{s}", .{ url, elapsed_ms, self.max_output_size, escaped }); } - return try std.fmt.allocPrint(allocator, "{{\"url\":\"{s}\",\"content\":\"{s}\"}}", .{ url, escaped }); + return try std.fmt.allocPrint(allocator, "Fetched content from {s} ({d}ms):\n\n{s}", .{ url, elapsed_ms, escaped }); } fn vtableReadOnly(ptr: *anyopaque) bool {