diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..2b9c927f
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,274 @@
+name: Release Desktop
+
+# Tag-driven release: push a tag like `v1.2.0` to build and publish the
+# desktop client for macOS (arm64 + x64) and Windows (x64). The tag is the
+# single source of truth for the version — it's written into package.json at
+# build time, so the maintainer never edits the version by hand.
+on:
+ push:
+ tags:
+ - "v*"
+ # Manual trigger for testing the full pipeline without cutting a real tag.
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version to stamp (e.g. 1.0.0-test). Used for package.json and R2 path."
+ type: string
+ default: "0.0.0-dev"
+ publish_r2:
+ description: "Upload installers to R2 + register in D1 (needs Cloudflare secrets)"
+ type: boolean
+ default: false
+
+permissions:
+ contents: write
+
+jobs:
+ build:
+ name: Build ${{ matrix.name }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ # Don't cancel the other platforms if one fails — we want to see all
+ # failures in a single run.
+ fail-fast: false
+ matrix:
+ include:
+ - name: macOS arm64
+ os: macos-14
+ platform: mac
+ arch: arm64
+ eb_flags: --mac --arm64
+ - name: macOS x64
+ os: macos-15-intel
+ platform: mac
+ arch: x64
+ eb_flags: --mac --x64
+ - name: Windows x64
+ os: windows-latest
+ platform: win
+ arch: x64
+ eb_flags: --win --x64
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Derive version
+ # Tag push: strip the leading "v" from GITHUB_REF_NAME (e.g. v1.2.0).
+ # Manual dispatch: use the provided version input.
+ id: ver
+ shell: bash
+ run: |
+ if [ "${{ github.event_name }}" = "push" ]; then
+ ref="${GITHUB_REF_NAME:-}"
+ echo "version=${ref#v}" >> "$GITHUB_OUTPUT"
+ else
+ echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
+ - name: Build Python backend (PyInstaller)
+ shell: bash
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r desktop/build/requirements-desktop.txt
+ pip install pyinstaller
+ # Run from repo root so the spec's relative datas resolve correctly.
+ pyinstaller desktop/build/cowagent-backend.spec \
+ --noconfirm \
+ --distpath desktop/build/dist \
+ --workpath desktop/build/build-work
+
+ - name: Install desktop deps
+ working-directory: desktop
+ run: npm ci
+
+ - name: Write version into package.json
+ working-directory: desktop
+ shell: bash
+ run: npm version "${{ steps.ver.outputs.version }}" --no-git-tag-version --allow-same-version
+
+ - name: Build & publish (electron-builder)
+ working-directory: desktop
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # Signing secrets are passed through as-is; we only export them to the
+ # environment below when non-empty. An empty CSC_LINK would make
+ # electron-builder try to load a bogus certificate and fail, so unset
+ # is the correct state for unsigned builds.
+ MAC_CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
+ MAC_CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
+ WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
+ run: |
+ npm run build
+
+ # Only export signing vars when provided. Empty strings are NOT the
+ # same as unset to electron-builder: an empty CSC_LINK is treated as
+ # a (broken) certificate path and aborts the build. Leaving them
+ # unset makes electron-builder fall back to an unsigned build.
+ if [ -n "$MAC_CSC_LINK" ]; then
+ export CSC_LINK="$MAC_CSC_LINK"
+ export CSC_KEY_PASSWORD="$MAC_CSC_KEY_PASSWORD"
+ fi
+ if [ -z "$WIN_CSC_LINK" ]; then
+ unset WIN_CSC_LINK WIN_CSC_KEY_PASSWORD
+ fi
+
+ # Publish to the GitHub Release on tag pushes; otherwise build only.
+ if [ "${{ github.event_name }}" = "push" ]; then
+ PUBLISH=always
+ else
+ PUBLISH=never
+ fi
+ npx electron-builder ${{ matrix.eb_flags }} --publish "$PUBLISH"
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ # One bundle per platform/arch so the publish job can collect them all.
+ name: cowagent-${{ matrix.platform }}-${{ matrix.arch }}
+ path: |
+ desktop/release/*.dmg
+ desktop/release/*.exe
+ desktop/release/*.yml
+ desktop/release/*.blockmap
+ if-no-files-found: ignore
+ retention-days: 7
+
+ # Mirror the release installers to R2 (CDN-backed) and register them in D1 so
+ # cowagent.ai/download/{platform}/latest can resolve and count downloads.
+ # Runs only on tag pushes, and is a no-op (skips) until the Cloudflare secrets
+ # are configured, so it never blocks unsigned/dry builds.
+ publish-r2:
+ name: Publish to R2 + D1
+ # Require every platform in the build matrix to succeed before publishing,
+ # so a release on R2/D1 is always complete (all installers present) rather
+ # than partial. needs: build already gates on all matrix jobs succeeding.
+ needs: build
+ runs-on: ubuntu-latest
+ # Run on a tag push, or on a manual dispatch when publish_r2 is checked.
+ if: >-
+ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) ||
+ (github.event_name == 'workflow_dispatch' && github.event.inputs.publish_r2 == 'true')
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Guard on Cloudflare secrets
+ id: guard
+ env:
+ CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ run: |
+ if [ -n "$CF_TOKEN" ]; then
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "enabled=false" >> "$GITHUB_OUTPUT"
+ echo "::notice::CLOUDFLARE_API_TOKEN not set — skipping R2/D1 publish."
+ fi
+
+ - name: Derive version
+ if: steps.guard.outputs.enabled == 'true'
+ id: ver
+ run: |
+ if [ "${{ github.event_name }}" = "push" ]; then
+ echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
+ else
+ echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Download all build artifacts
+ if: steps.guard.outputs.enabled == 'true'
+ uses: actions/download-artifact@v4
+ with:
+ path: artifacts
+
+ - name: Stage installers
+ if: steps.guard.outputs.enabled == 'true'
+ id: stage
+ run: |
+ mkdir -p dist
+ # Flatten installers from every per-platform artifact dir; only the
+ # user-facing installers go to R2 (updater .yml/.blockmap stay on the
+ # GitHub Release, which electron-updater reads directly).
+ find artifacts -type f \( -name '*.dmg' -o -name '*.exe' \) -exec cp {} dist/ \;
+ echo "Staged files:"; ls -la dist
+ # When the whole matrix failed there's nothing to publish; flag it so
+ # the R2/D1 steps skip instead of writing an empty/partial release.
+ if [ -n "$(ls -A dist 2>/dev/null)" ]; then
+ echo "has_files=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "has_files=false" >> "$GITHUB_OUTPUT"
+ echo "::warning::No installers found in any artifact — skipping R2/D1 publish."
+ fi
+
+ - name: Upload installers to R2
+ if: steps.guard.outputs.enabled == 'true' && steps.stage.outputs.has_files == 'true'
+ env:
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ VER: ${{ steps.ver.outputs.version }}
+ run: |
+ # Reuse the existing cow-skills bucket under a desktop/ prefix; this
+ # is served by the cdn.cowagent.ai custom domain.
+ for f in dist/*; do
+ base="$(basename "$f")"
+ key="desktop/v${VER}/${base}"
+ echo "==> Uploading $base -> r2://cow-skills/$key"
+ npx --yes wrangler@latest r2 object put "cow-skills/$key" \
+ --file "$f" --remote
+ done
+
+ - name: Register release rows in D1
+ if: steps.guard.outputs.enabled == 'true' && steps.stage.outputs.has_files == 'true'
+ env:
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ VER: ${{ steps.ver.outputs.version }}
+ run: |
+ # Map each installer filename to a platform id. dmg arch is in the
+ # name (…-arm64.dmg / …-x64.dmg); .exe is the Windows installer.
+ sql_file="$(mktemp)"
+
+ # Pre-releases (e.g. 1.0.0-test / -beta / -rc.1 / -alpha / -dev) are
+ # recorded but NEVER marked latest, so /download/
/latest keeps
+ # serving the last stable build. They also must not clear an existing
+ # stable's latest flag. Only a final version (no pre-release suffix)
+ # becomes the new latest and clears the previous one per platform.
+ case "$VER" in
+ *-*) is_latest=0; echo "==> $VER is a pre-release; not marking latest." ;;
+ *) is_latest=1; echo "==> $VER is a stable release; marking latest." ;;
+ esac
+
+ for f in dist/*; do
+ base="$(basename "$f")"
+ size="$(stat -c%s "$f")"
+ case "$base" in
+ *arm64.dmg) platform="mac-arm64" ;;
+ *x64.dmg) platform="mac-x64" ;;
+ *.exe) platform="win" ;;
+ *) echo "Skipping unrecognized artifact: $base"; continue ;;
+ esac
+ key="v${VER}/${base}"
+ # Stable only: clear the previous latest for THIS platform first, so
+ # a partial backfill never wipes other platforms' latest flag.
+ if [ "$is_latest" = "1" ]; then
+ echo "UPDATE releases SET is_latest = 0 WHERE platform = '${platform}';" >> "$sql_file"
+ fi
+ echo "INSERT OR REPLACE INTO releases (version, platform, filename, size, is_latest) VALUES ('${VER}', '${platform}', '${key}', ${size}, ${is_latest});" >> "$sql_file"
+ done
+ echo "==> D1 statements:"; cat "$sql_file"
+ npx --yes wrangler@latest d1 execute cow-desktop --remote --file "$sql_file"
diff --git a/.gitignore b/.gitignore
index de10c0b7..1d73ecf6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,16 @@ dist/
build/
*.egg-info/
.cow.pid
+
+# Desktop backend packaging: keep the source files (spec/requirements/script)
+# tracked even though the generic build/ rule above ignores them, but never
+# track the build outputs or local venv.
+!desktop/build/
+desktop/build/*
+!desktop/build/cowagent-backend.spec
+!desktop/build/requirements-desktop.txt
+!desktop/build/build-backend.sh
+
+# Icon authoring scratch dir: intermediate assets used to produce the final
+# icons. Only the finished icons under desktop/resources/ should be committed.
+desktop/resources/.icon-work/
diff --git a/app.py b/app.py
index cee532bd..7e7bc3bb 100644
--- a/app.py
+++ b/app.py
@@ -15,6 +15,11 @@ import threading
_channel_mgr = None
+# Desktop mode: a lighter runtime for the packaged Electron client. The plugin
+# framework is still bundled (it's tiny and on the web channel's import path),
+# but we skip loading actual plugins and MCP tools to keep startup fast.
+DESKTOP_MODE = os.environ.get("COW_DESKTOP") == "1"
+
def get_channel_manager():
return _channel_mgr
@@ -75,7 +80,7 @@ class ChannelManager:
if self._primary_channel is None and channels:
self._primary_channel = channels[0][1]
- if first_start:
+ if first_start and not DESKTOP_MODE:
PluginManager().load_plugins()
# Cloud client is optional. It is only started when
@@ -364,10 +369,18 @@ def run():
_sync_builtin_skills()
# Kick off MCP server loading in the background so first-message
- # latency isn't dominated by npx package downloads.
- _warmup_mcp_tools()
+ # latency isn't dominated by npx package downloads. Skipped in desktop
+ # mode (MCP relies on external npx/uvx runtimes that aren't bundled).
+ if not DESKTOP_MODE:
+ _warmup_mcp_tools()
- _warmup_scheduler()
+ if DESKTOP_MODE:
+ # Defer the (heavy) AgentBridge/scheduler warmup to a background
+ # thread so the web API becomes available within a couple seconds.
+ # The scheduler still starts; it just doesn't block UI readiness.
+ threading.Thread(target=_warmup_scheduler, daemon=True).start()
+ else:
+ _warmup_scheduler()
logger.info(f"[App] Starting channels: {channel_names}")
diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py
index c4c17169..7353cea7 100644
--- a/channel/web/web_channel.py
+++ b/channel/web/web_channel.py
@@ -24,7 +24,7 @@ from common import const
from common import i18n
from common.log import logger
from common.singleton import singleton
-from config import conf
+from config import conf, get_data_root, get_weixin_credentials_path
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"}
VIDEO_EXTENSIONS = {".mp4", ".webm", ".avi", ".mov", ".mkv"}
@@ -1114,18 +1114,25 @@ class WebChannel(ChatChannel):
else:
logger.info(f"[WebChannel] 🔒 Listening on {host} only (local access). For public access, set web_host to 0.0.0.0 and configure web_password")
- try:
- import webbrowser
- webbrowser.open(f"http://localhost:{port}")
- logger.debug(f"[WebChannel] Opened browser at http://localhost:{port}")
- except Exception as e:
- logger.debug(f"[WebChannel] Could not open browser: {e}")
+ # In desktop mode the Electron shell renders the UI, so don't pop a
+ # browser window (also avoids issues when running detached/headless).
+ if os.environ.get("COW_DESKTOP") != "1":
+ try:
+ import webbrowser
+ webbrowser.open(f"http://localhost:{port}")
+ logger.debug(f"[WebChannel] Opened browser at http://localhost:{port}")
+ except Exception as e:
+ logger.debug(f"[WebChannel] Could not open browser: {e}")
- # 确保静态文件目录存在
+ # Ensure the static dir exists. In a packaged build it ships read-only
+ # inside the bundle, so swallow errors instead of failing startup.
static_dir = os.path.join(os.path.dirname(__file__), 'static')
if not os.path.exists(static_dir):
- os.makedirs(static_dir)
- logger.debug(f"[WebChannel] Created static directory: {static_dir}")
+ try:
+ os.makedirs(static_dir)
+ logger.debug(f"[WebChannel] Created static directory: {static_dir}")
+ except OSError as e:
+ logger.debug(f"[WebChannel] Skipped creating static dir (read-only bundle?): {e}")
urls = (
'/', 'RootHandler',
@@ -1730,8 +1737,7 @@ class ConfigHandler:
if not applied:
return json.dumps({"status": "error", "message": "no valid keys to update"})
- config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(
- os.path.abspath(__file__)))), "config.json")
+ config_path = os.path.join(get_data_root(), "config.json")
if os.path.exists(config_path):
with open(config_path, "r", encoding="utf-8") as f:
file_cfg = json.load(f)
@@ -2164,10 +2170,7 @@ class ModelsHandler:
@staticmethod
def _config_path() -> str:
- return os.path.join(
- os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
- "config.json",
- )
+ return os.path.join(get_data_root(), "config.json")
@classmethod
def _read_file_config(cls) -> dict:
@@ -3550,8 +3553,12 @@ class ChannelsHandler:
try:
local_config = conf()
active_channels = self._active_channel_set()
+ # Desktop build ships without lark-oapi, so hide Feishu from the list.
+ desktop_mode = os.environ.get("COW_DESKTOP") == "1"
channels = []
for ch_name, ch_def in self.CHANNEL_DEFS.items():
+ if desktop_mode and ch_name == "feishu":
+ continue
fields_out = []
for f in ch_def["fields"]:
raw_val = local_config.get(f["key"], f.get("default", ""))
@@ -3633,8 +3640,7 @@ class ChannelsHandler:
if not applied:
return json.dumps({"status": "error", "message": "no valid fields to update"})
- config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(
- os.path.abspath(__file__)))), "config.json")
+ config_path = os.path.join(get_data_root(), "config.json")
if os.path.exists(config_path):
with open(config_path, "r", encoding="utf-8") as f:
file_cfg = json.load(f)
@@ -3704,8 +3710,7 @@ class ChannelsHandler:
new_channel_type = ",".join(existing)
local_config["channel_type"] = new_channel_type
- config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(
- os.path.abspath(__file__)))), "config.json")
+ config_path = os.path.join(get_data_root(), "config.json")
if os.path.exists(config_path):
with open(config_path, "r", encoding="utf-8") as f:
file_cfg = json.load(f)
@@ -3760,8 +3765,7 @@ class ChannelsHandler:
local_config = conf()
local_config["channel_type"] = new_channel_type
- config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(
- os.path.abspath(__file__)))), "config.json")
+ config_path = os.path.join(get_data_root(), "config.json")
if os.path.exists(config_path):
with open(config_path, "r", encoding="utf-8") as f:
file_cfg = json.load(f)
@@ -3911,9 +3915,7 @@ class WeixinQrHandler:
if not bot_token or not bot_id:
return json.dumps({"status": "error", "message": "Login confirmed but missing token"})
- cred_path = os.path.expanduser(
- conf().get("weixin_credentials_path", "~/.weixin_cow_credentials.json")
- )
+ cred_path = get_weixin_credentials_path()
from channel.weixin.weixin_channel import _save_credentials
_save_credentials(cred_path, {
"token": bot_token,
@@ -4577,8 +4579,7 @@ class LogsHandler:
web.header('Cache-Control', 'no-cache')
web.header('X-Accel-Buffering', 'no')
- from config import get_root
- log_path = os.path.join(get_root(), "run.log")
+ log_path = os.path.join(get_data_root(), "run.log")
def generate():
if not os.path.isfile(log_path):
diff --git a/channel/weixin/weixin_channel.py b/channel/weixin/weixin_channel.py
index 309aecab..516b041a 100644
--- a/channel/weixin/weixin_channel.py
+++ b/channel/weixin/weixin_channel.py
@@ -24,7 +24,7 @@ from channel.weixin.weixin_message import WeixinMessage
from common.expired_dict import ExpiredDict
from common.log import logger
from common.singleton import singleton
-from config import conf
+from config import conf, get_weixin_credentials_path
MAX_CONSECUTIVE_FAILURES = 3
BACKOFF_DELAY = 30
@@ -96,9 +96,7 @@ class WeixinChannel(ChatChannel):
cdn_base_url = conf().get("weixin_cdn_base_url", CDN_BASE_URL)
token = conf().get("weixin_token", "")
- self._credentials_path = os.path.expanduser(
- conf().get("weixin_credentials_path", "~/.weixin_cow_credentials.json")
- )
+ self._credentials_path = get_weixin_credentials_path()
# Always load credentials so we can restore context_tokens even when
# the bot token itself comes from config.
diff --git a/common/cloud_client.py b/common/cloud_client.py
index 79d9b248..30d7f7d5 100644
--- a/common/cloud_client.py
+++ b/common/cloud_client.py
@@ -21,7 +21,7 @@ from bridge.context import Context, ContextType
from bridge.reply import Reply, ReplyType
from common.log import logger
from linkai import LinkAIClient, PushMsg
-from config import conf, pconf, plugin_config, available_setting, write_plugin_config, get_root
+from config import conf, pconf, plugin_config, available_setting, write_plugin_config, get_root, get_weixin_credentials_path
from plugins import PluginManager
import threading
import time
@@ -336,9 +336,7 @@ class CloudClient(LinkAIClient):
@staticmethod
def _remove_weixin_credentials():
"""Remove the weixin token credentials file so next connect triggers QR login."""
- cred_path = os.path.expanduser(
- conf().get("weixin_credentials_path", "~/.weixin_cow_credentials.json")
- )
+ cred_path = get_weixin_credentials_path()
try:
if os.path.exists(cred_path):
os.remove(cred_path)
diff --git a/common/log.py b/common/log.py
index e0bc577c..c310703c 100644
--- a/common/log.py
+++ b/common/log.py
@@ -1,8 +1,21 @@
import logging
+import os
import sys
import io
+def _log_path():
+ # Mirror config.get_data_root() without importing config (avoids a circular
+ # import, since config imports this module). The desktop build sets
+ # COW_DATA_DIR (e.g. ~/.cow); source deployments fall back to CWD.
+ data_dir = os.environ.get("COW_DATA_DIR")
+ if data_dir:
+ data_dir = os.path.expanduser(data_dir)
+ os.makedirs(data_dir, exist_ok=True)
+ return os.path.join(data_dir, "run.log")
+ return "run.log"
+
+
def _reset_logger(log):
for handler in log.handlers:
handler.close()
@@ -20,7 +33,7 @@ def _reset_logger(log):
datefmt="%Y-%m-%d %H:%M:%S",
)
)
- file_handle = logging.FileHandler("run.log", encoding="utf-8")
+ file_handle = logging.FileHandler(_log_path(), encoding="utf-8")
file_handle.setFormatter(
logging.Formatter(
"[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d] - %(message)s",
diff --git a/common/tmp_dir.py b/common/tmp_dir.py
index b01880bd..b0d89bc8 100644
--- a/common/tmp_dir.py
+++ b/common/tmp_dir.py
@@ -1,18 +1,21 @@
import os
-import pathlib
+from common.utils import expand_path
from config import conf
class TmpDir(object):
- """A temporary directory that is deleted when the object is destroyed."""
+ """Temporary directory for transient artifacts (e.g. synthesized voice).
- tmpFilePath = pathlib.Path("./tmp/")
+ Resolves to ``/tmp`` (default ``~/cow/tmp``) so temp files
+ land inside the agent workspace instead of a CWD-relative ``./tmp``, which
+ is unreliable for the packaged desktop app where CWD is undefined.
+ """
def __init__(self):
- pathExists = os.path.exists(self.tmpFilePath)
- if not pathExists:
- os.makedirs(self.tmpFilePath)
+ ws_root = expand_path(conf().get("agent_workspace", "~/cow"))
+ self.tmpFilePath = os.path.join(ws_root, "tmp")
+ os.makedirs(self.tmpFilePath, exist_ok=True)
def path(self):
return str(self.tmpFilePath) + "/"
diff --git a/config.py b/config.py
index baf8e0b2..03bf9abe 100644
--- a/config.py
+++ b/config.py
@@ -5,6 +5,7 @@ import json
import logging
import os
import pickle
+import sys
from common.log import logger
from common import i18n
@@ -377,10 +378,16 @@ def load_config():
logger.info(" \\____\\___/ \\_/\\_//_/ \\_\\__, |\\___|_| |_|\\__|")
logger.info(" |___/ ")
logger.info("")
- config_path = "./config.json"
+ # User config lives in the data root: source deployments use CWD (./), while
+ # the desktop build points COW_DATA_DIR at ~/.cow so config survives updates.
+ config_path = os.path.join(get_data_root(), "config.json")
if not os.path.exists(config_path):
logger.info("config file not found, falling back to config-template.json")
- config_path = "./config-template.json"
+ # Resolve the template via get_resource_root() so it works both from
+ # source and from a frozen (PyInstaller) bundle, where the template
+ # ships inside the bundle (sys._MEIPASS) and CWD may differ.
+ template_path = os.path.join(get_resource_root(), "config-template.json")
+ config_path = template_path if os.path.exists(template_path) else "./config-template.json"
config_str = read_file(config_path)
logger.debug("[INIT] config str: {}".format(drag_sensitive(config_str)))
@@ -620,6 +627,34 @@ def get_root():
return os.path.dirname(os.path.abspath(__file__))
+def get_resource_root():
+ """Directory holding bundled read-only resources (e.g. config-template.json).
+
+ Under PyInstaller, data files live in sys._MEIPASS (the onedir _internal
+ folder), which differs from get_root() — the latter is used for writable
+ user data and should stay next to the executable, not inside the bundle.
+ """
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ return sys._MEIPASS
+ return os.path.dirname(os.path.abspath(__file__))
+
+
+def get_data_root():
+ """Directory for writable user data (config.json, user_datas.pkl, run.log).
+
+ The desktop build sets COW_DATA_DIR (e.g. ~/.cow) so data lives in the
+ user's home rather than inside the read-only app bundle and survives app
+ updates. When unset (source deployment), it falls back to get_root(), so
+ existing behavior is unchanged.
+ """
+ data_dir = os.environ.get("COW_DATA_DIR")
+ if data_dir:
+ data_dir = os.path.expanduser(data_dir)
+ os.makedirs(data_dir, exist_ok=True)
+ return data_dir
+ return get_root()
+
+
def read_file(path):
with open(path, mode="r", encoding="utf-8-sig") as f:
return f.read()
@@ -630,13 +665,29 @@ def conf():
def get_appdata_dir():
- data_path = os.path.join(get_root(), conf().get("appdata_dir", ""))
+ data_path = os.path.join(get_data_root(), conf().get("appdata_dir", ""))
if not os.path.exists(data_path):
logger.info("[INIT] data path not exists, create it: {}".format(data_path))
os.makedirs(data_path)
return data_path
+def get_weixin_credentials_path():
+ """Resolve the Weixin credentials (token) file path.
+
+ Honors an explicit ``weixin_credentials_path`` from config. Otherwise the
+ packaged desktop build (COW_DATA_DIR set) keeps it under the data dir
+ (~/.cow) so all user data stays together, while source deployments retain
+ the legacy ~/.weixin_cow_credentials.json default unchanged.
+ """
+ configured = conf().get("weixin_credentials_path")
+ if configured:
+ return os.path.expanduser(configured)
+ if os.environ.get("COW_DATA_DIR"):
+ return os.path.join(get_data_root(), "weixin_credentials.json")
+ return os.path.expanduser("~/.weixin_cow_credentials.json")
+
+
def subscribe_msg():
trigger_prefix = conf().get("single_chat_prefix", [""])[0]
msg = conf().get("subscribe_msg", "")
diff --git a/desktop/.gitignore b/desktop/.gitignore
new file mode 100644
index 00000000..7824fd01
--- /dev/null
+++ b/desktop/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+dist/
+release/
+*.log
+.DS_Store
+Thumbs.db
diff --git a/desktop/README.md b/desktop/README.md
new file mode 100644
index 00000000..dacf8a30
--- /dev/null
+++ b/desktop/README.md
@@ -0,0 +1,81 @@
+# CowAgent Desktop
+
+Cross-platform desktop client for CowAgent, built with Electron + React + TypeScript.
+
+## Development
+
+### Prerequisites
+
+- Node.js 18+
+- npm or yarn
+- Python 3.7+ (for the backend)
+
+### Setup
+
+```bash
+cd desktop
+npm install
+```
+
+### Run in Development
+
+Start the renderer dev server and Electron together:
+
+```bash
+npm run dev
+```
+
+Or run them separately:
+
+```bash
+# Terminal 1: Start Vite dev server
+npm run dev:renderer
+
+# Terminal 2: Start Electron (after renderer is ready)
+npm run dev:main
+```
+
+The app will automatically start the Python backend from the parent directory.
+
+### Build
+
+```bash
+# Build for current platform
+npm run dist
+
+# Build for macOS only
+npm run dist:mac
+
+# Build for Windows only
+npm run dist:win
+```
+
+Build outputs are placed in the `release/` directory.
+
+## Architecture
+
+```
+desktop/
+├── src/
+│ ├── main/ # Electron main process
+│ │ ├── index.ts # Window management, IPC
+│ │ ├── python-manager.ts # Python backend lifecycle
+│ │ └── preload.ts # Context bridge for renderer
+│ └── renderer/ # React UI (Vite)
+│ └── src/
+│ ├── api/ # HTTP client for backend APIs
+│ ├── components/ # Reusable UI components
+│ ├── hooks/ # React hooks
+│ ├── pages/ # Page components
+│ └── types.ts # TypeScript types
+├── resources/ # App icons
+├── package.json # Dependencies and build config
+└── vite.config.ts # Vite config
+```
+
+### How it Works
+
+1. Electron main process starts and creates the app window
+2. It spawns the Python backend (`app.py`) as a child process
+3. The React UI communicates with the Python backend via HTTP APIs
+4. SSE (Server-Sent Events) is used for streaming chat responses and live logs
diff --git a/desktop/build/build-backend.sh b/desktop/build/build-backend.sh
new file mode 100755
index 00000000..940c1afa
--- /dev/null
+++ b/desktop/build/build-backend.sh
@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+#
+# Build the desktop backend into a self-contained onedir bundle via PyInstaller.
+# Run from anywhere; paths are resolved relative to the repo root.
+#
+# Usage:
+# bash desktop/build/build-backend.sh # build
+# PYTHON=python3.11 bash desktop/build/build-backend.sh # pick interpreter
+#
+# Output: desktop/build/dist/cowagent-backend/ (folder with the executable)
+set -euo pipefail
+
+# --- resolve paths --------------------------------------------------------
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+BUILD_DIR="$SCRIPT_DIR"
+VENV_DIR="$BUILD_DIR/.venv-build"
+
+# Prefer Python 3.11 when available: on 3.13+ web.py must be installed from a
+# GitHub git source (the PyPI build fails), which is flaky on some networks.
+# 3.11 installs web.py straight from PyPI and has the best PyInstaller support.
+if [ -z "${PYTHON:-}" ]; then
+ for cand in \
+ "/Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11" \
+ "python3.11" \
+ "python3.12" \
+ "python3"; do
+ if command -v "$cand" >/dev/null 2>&1; then
+ PYTHON="$cand"
+ break
+ fi
+ done
+fi
+# Prefer Python 3.11: it installs web.py from PyPI (no GitHub clone) and avoids
+# 3.13's removed-cgi compatibility shims. Override with PYTHON=... if needed.
+pick_python() {
+ if [ -n "${PYTHON:-}" ]; then echo "$PYTHON"; return; fi
+ for c in python3.11 python3.12 python3.10 python3; do
+ if command -v "$c" >/dev/null 2>&1; then echo "$c"; return; fi
+ done
+ echo python3
+}
+PYTHON="$(pick_python)"
+
+echo "==> Repo root: $ROOT"
+echo "==> Using Python: $($PYTHON --version 2>&1) ($PYTHON)"
+
+# --- isolated build venv --------------------------------------------------
+if [ ! -d "$VENV_DIR" ]; then
+ echo "==> Creating build venv at $VENV_DIR"
+ "$PYTHON" -m venv "$VENV_DIR"
+fi
+# shellcheck disable=SC1091
+source "$VENV_DIR/bin/activate"
+
+echo "==> Installing build dependencies"
+pip install -q --upgrade pip
+# Don't leave a half-populated venv behind if deps fail (e.g. flaky network):
+# the next run would otherwise reuse a broken venv.
+if ! pip install -q -r "$BUILD_DIR/requirements-desktop.txt"; then
+ echo "!! Dependency install failed. Removing the build venv so a retry starts clean." >&2
+ deactivate || true
+ rm -rf "$VENV_DIR"
+ exit 1
+fi
+pip install -q pyinstaller
+
+# --- run pyinstaller from repo root so relative datas resolve -------------
+cd "$ROOT"
+echo "==> Running PyInstaller (onedir)"
+pyinstaller "$BUILD_DIR/cowagent-backend.spec" \
+ --noconfirm \
+ --distpath "$BUILD_DIR/dist" \
+ --workpath "$BUILD_DIR/build-work"
+
+echo ""
+echo "==> Done. Bundle at: $BUILD_DIR/dist/cowagent-backend/"
+du -sh "$BUILD_DIR/dist/cowagent-backend/" 2>/dev/null || true
+echo "==> Smoke test: COW_DESKTOP=1 \"$BUILD_DIR/dist/cowagent-backend/cowagent-backend\""
diff --git a/desktop/build/cowagent-backend.spec b/desktop/build/cowagent-backend.spec
new file mode 100644
index 00000000..9e20841e
--- /dev/null
+++ b/desktop/build/cowagent-backend.spec
@@ -0,0 +1,145 @@
+# -*- mode: python ; coding: utf-8 -*-
+"""
+PyInstaller spec for the CowAgent desktop backend (onedir).
+
+Produces a self-contained `cowagent-backend` folder that the Electron app
+spawns directly, so end users don't need Python installed.
+
+onedir is chosen over onefile because the backend reads data files via paths
+relative to the source tree (e.g. config-template.json, skills/, chat.html);
+onedir preserves that layout, while onefile's temp-extraction would break it.
+
+Build from the repo root:
+ pyinstaller desktop/build/cowagent-backend.spec --noconfirm
+"""
+import os
+from PyInstaller.utils.hooks import collect_submodules, collect_data_files
+
+# Resolve the repo root from the spec's own location (desktop/build/ -> root),
+# independent of the current working directory. PyInstaller exposes SPECPATH.
+ROOT = os.path.abspath(os.path.join(SPECPATH, '..', '..'))
+
+
+def rp(*parts):
+ """Absolute path under the repo root."""
+ return os.path.join(ROOT, *parts)
+
+# --- Hidden imports -------------------------------------------------------
+# Channels are imported dynamically by channel_factory via string names, so
+# PyInstaller's static analysis can't see them. List every channel we ship
+# (Feishu is intentionally excluded — lark-oapi is dropped from the desktop
+# build to save ~116MB).
+hiddenimports = [
+ # channels (dynamic import in channel/channel_factory.py)
+ 'channel.web.web_channel',
+ 'channel.terminal.terminal_channel',
+ 'channel.weixin.weixin_channel',
+ 'channel.wechatmp.wechatmp_channel',
+ 'channel.wechatcom.wechatcomapp_channel',
+ 'channel.wechat_kf.wechat_kf_channel',
+ 'channel.dingtalk.dingtalk_channel',
+ 'channel.wecom_bot.wecom_bot_channel',
+ 'channel.qq.qq_channel',
+ 'channel.telegram.telegram_channel',
+ 'channel.slack.slack_channel',
+ 'channel.discord.discord_channel',
+]
+
+# Agent tools and model providers are imported lazily in places; collect their
+# submodules so nothing is missed at runtime.
+hiddenimports += collect_submodules('agent.tools')
+hiddenimports += collect_submodules('models')
+hiddenimports += collect_submodules('voice')
+hiddenimports += collect_submodules('bridge')
+
+# Plugin framework: WebChannel -> ChatChannel imports `from plugins import *`,
+# so the framework package must be present even though desktop mode never loads
+# actual plugins (it's only ~tens of KB of code).
+hiddenimports += [
+ 'plugins',
+ 'plugins.event',
+ 'plugins.plugin',
+ 'plugins.plugin_manager',
+]
+
+# Third-party SDKs that use lazy/conditional imports internally.
+hiddenimports += collect_submodules('dashscope')
+hiddenimports += [
+ 'tiktoken_ext',
+ 'tiktoken_ext.openai_public',
+]
+
+# --- Data files -----------------------------------------------------------
+# Runtime-read files/dirs that must travel with the executable. Paths are
+# (source, dest_dir_in_bundle).
+datas = [
+ (rp('config-template.json'), '.'),
+ (rp('skills'), 'skills'),
+ # Web console served on the backend port: ship chat.html plus its static
+ # assets (~1.9MB) so the browser-based console works as a debug/fallback
+ # entry alongside the Electron UI.
+ (rp('channel', 'web', 'chat.html'), 'channel/web'),
+ (rp('channel', 'web', 'static'), 'channel/web/static'),
+]
+
+# Some libraries (tiktoken encodings, etc.) ship data files.
+datas += collect_data_files('tiktoken_ext', include_py_files=False)
+
+# --- Excludes -------------------------------------------------------------
+# Keep the bundle lean: drop Feishu's heavy SDK, plugins (disabled in desktop
+# mode), tests/docs, and dev-only packages.
+excludes = [
+ 'lark_oapi', # Feishu — ~116MB, excluded from desktop build
+ 'tests',
+ 'pip',
+ 'wheel',
+ 'pytest',
+ 'playwright', # browser tool is opt-in, not bundled
+]
+
+block_cipher = None
+
+a = Analysis(
+ [rp('app.py')],
+ pathex=[ROOT],
+ binaries=[],
+ datas=datas,
+ hiddenimports=hiddenimports,
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=excludes,
+ win_no_prefer_redirects=False,
+ win_private_assemblies=False,
+ cipher=block_cipher,
+ noarchive=False,
+)
+
+pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ [],
+ exclude_binaries=True,
+ name='cowagent-backend',
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=False,
+ console=True,
+ disable_windowed_traceback=False,
+ target_arch=None,
+ codesign_identity=None,
+ entitlements_file=None,
+)
+
+coll = COLLECT(
+ exe,
+ a.binaries,
+ a.datas,
+ strip=False,
+ upx=False,
+ upx_exclude=[],
+ name='cowagent-backend',
+)
diff --git a/desktop/build/requirements-desktop.txt b/desktop/build/requirements-desktop.txt
new file mode 100644
index 00000000..dc6c0776
--- /dev/null
+++ b/desktop/build/requirements-desktop.txt
@@ -0,0 +1,52 @@
+# Desktop backend dependencies (slimmed down from the full requirements).
+#
+# Goal: keep the package light. The desktop client only needs the web channel
+# (which Electron talks to) plus the agent core; the remaining IM channels are
+# cheap (~27MB total) so we keep them, but Feishu's `lark-oapi` (~116MB) is
+# dropped — it is by far the heaviest dependency and not needed for a C-end
+# desktop app. Feishu is hidden in desktop mode (see COW_DESKTOP in app.py).
+
+# ---- core ----
+numpy>=1.21
+aiohttp>=3.8.6,<3.10
+requests>=2.28.2
+chardet>=5.1.0
+Pillow
+python-dotenv>=1.0.0
+PyYAML>=6.0
+croniter>=2.0.0
+click>=8.0
+qrcode
+json-repair
+
+# ---- web framework (web channel) ----
+# web.py 0.62 fails to build on Python 3.13+ (cgi removed); use the GitHub fix.
+web.py; python_version < "3.13"
+web.py @ git+https://github.com/webpy/webpy.git ; python_version >= "3.13"
+legacy-cgi; python_version >= "3.13"
+
+# ---- AI model SDKs ----
+zai-sdk
+dashscope
+tenacity # used by some dashscope submodules (retry logic)
+google-generativeai
+tiktoken>=0.3.2
+
+# ---- voice (TTS/ASR) — kept per product decision ----
+pydub>=0.25.1
+gTTS>=2.3.1
+
+# ---- document parsing (web_fetch / knowledge) ----
+pypdf
+python-docx
+openpyxl
+python-pptx
+
+# ---- IM channels (kept; lightweight). Feishu/lark-oapi intentionally excluded. ----
+wechatpy
+pycryptodome
+dingtalk_stream
+websocket-client>=1.4.0
+python-telegram-bot
+slack_bolt
+discord.py
diff --git a/desktop/package-lock.json b/desktop/package-lock.json
new file mode 100644
index 00000000..24575efa
--- /dev/null
+++ b/desktop/package-lock.json
@@ -0,0 +1,8300 @@
+{
+ "name": "cowagent-desktop",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "cowagent-desktop",
+ "version": "1.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "electron-updater": "^6.8.9",
+ "highlight.js": "^11.11.1",
+ "lucide-react": "^1.21.0",
+ "markdown-it": "^14.2.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0",
+ "zustand": "^5.0.14"
+ },
+ "devDependencies": {
+ "@types/markdown-it": "^14.1.2",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.20",
+ "concurrently": "^9.1.0",
+ "electron": "^33.2.0",
+ "electron-builder": "^25.1.8",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.15",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@develar/schema-utils": {
+ "version": "2.6.5",
+ "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
+ "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.0",
+ "ajv-keywords": "^3.4.1"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/@electron/asar": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
+ "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^5.0.0",
+ "glob": "^7.1.6",
+ "minimatch": "^3.0.4"
+ },
+ "bin": {
+ "asar": "bin/asar.js"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/@electron/asar/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@electron/asar/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@electron/asar/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@electron/get": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
+ "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "env-paths": "^2.2.0",
+ "fs-extra": "^8.1.0",
+ "got": "^11.8.5",
+ "progress": "^2.0.3",
+ "semver": "^6.2.0",
+ "sumchecker": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "global-agent": "^3.0.0"
+ }
+ },
+ "node_modules/@electron/notarize": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
+ "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "fs-extra": "^9.0.1",
+ "promise-retry": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@electron/notarize/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@electron/notarize/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@electron/notarize/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@electron/osx-sign": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz",
+ "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "compare-version": "^0.1.2",
+ "debug": "^4.3.4",
+ "fs-extra": "^10.0.0",
+ "isbinaryfile": "^4.0.8",
+ "minimist": "^1.2.6",
+ "plist": "^3.0.5"
+ },
+ "bin": {
+ "electron-osx-flat": "bin/electron-osx-flat.js",
+ "electron-osx-sign": "bin/electron-osx-sign.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/isbinaryfile": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
+ "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/gjtorikian/"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@electron/osx-sign/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@electron/rebuild": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz",
+ "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@malept/cross-spawn-promise": "^2.0.0",
+ "chalk": "^4.0.0",
+ "debug": "^4.1.1",
+ "detect-libc": "^2.0.1",
+ "fs-extra": "^10.0.0",
+ "got": "^11.7.0",
+ "node-abi": "^3.45.0",
+ "node-api-version": "^0.2.0",
+ "node-gyp": "^9.0.0",
+ "ora": "^5.1.0",
+ "read-binary-file-arch": "^1.0.6",
+ "semver": "^7.3.5",
+ "tar": "^6.0.5",
+ "yargs": "^17.0.1"
+ },
+ "bin": {
+ "electron-rebuild": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=12.13.0"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@electron/rebuild/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@electron/universal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz",
+ "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@electron/asar": "^3.2.7",
+ "@malept/cross-spawn-promise": "^2.0.0",
+ "debug": "^4.3.1",
+ "dir-compare": "^4.2.0",
+ "fs-extra": "^11.1.1",
+ "minimatch": "^9.0.3",
+ "plist": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=16.4"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@electron/universal/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/fs-extra": {
+ "version": "11.3.4",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
+ "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@electron/universal/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@gar/promisify": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
+ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@malept/cross-spawn-promise": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
+ "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/malept"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund"
+ }
+ ],
+ "license": "Apache-2.0",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz",
+ "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "fs-extra": "^9.0.0",
+ "lodash": "^4.17.15",
+ "tmp-promise": "^3.0.2"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/@malept/flatpak-bundler/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@npmcli/fs": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz",
+ "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@gar/promisify": "^1.1.3",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@npmcli/fs/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@npmcli/move-file": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz",
+ "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==",
+ "deprecated": "This functionality has been moved to @npmcli/fs",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mkdirp": "^1.0.4",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@szmarczak/http-timer": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
+ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "defer-to-connect": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/cacheable-request": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
+ "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-cache-semantics": "*",
+ "@types/keyv": "^3.1.4",
+ "@types/node": "*",
+ "@types/responselike": "^1.0.0"
+ }
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/fs-extra": {
+ "version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
+ "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/keyv": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
+ "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.37",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
+ "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/plist": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
+ "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*",
+ "xmlbuilder": ">=11.0.1"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/responselike": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
+ "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/verror": {
+ "version": "1.10.11",
+ "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
+ "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.11",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
+ "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/7zip-bin": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
+ "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/app-builder-bin": {
+ "version": "5.0.0-alpha.10",
+ "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz",
+ "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/app-builder-lib": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz",
+ "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@develar/schema-utils": "~2.6.5",
+ "@electron/notarize": "2.5.0",
+ "@electron/osx-sign": "1.3.1",
+ "@electron/rebuild": "3.6.1",
+ "@electron/universal": "2.0.1",
+ "@malept/flatpak-bundler": "^0.4.0",
+ "@types/fs-extra": "9.0.13",
+ "async-exit-hook": "^2.0.1",
+ "bluebird-lst": "^1.0.9",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "chromium-pickle-js": "^0.2.0",
+ "config-file-ts": "0.2.8-rc1",
+ "debug": "^4.3.4",
+ "dotenv": "^16.4.5",
+ "dotenv-expand": "^11.0.6",
+ "ejs": "^3.1.8",
+ "electron-publish": "25.1.7",
+ "form-data": "^4.0.0",
+ "fs-extra": "^10.1.0",
+ "hosted-git-info": "^4.1.0",
+ "is-ci": "^3.0.0",
+ "isbinaryfile": "^5.0.0",
+ "js-yaml": "^4.1.0",
+ "json5": "^2.2.3",
+ "lazy-val": "^1.0.5",
+ "minimatch": "^10.0.0",
+ "resedit": "^1.7.0",
+ "sanitize-filename": "^1.6.3",
+ "semver": "^7.3.8",
+ "tar": "^6.1.12",
+ "temp-file": "^3.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "dmg-builder": "25.1.8",
+ "electron-builder-squirrel-windows": "25.1.8"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/app-builder-lib/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/aproba": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
+ "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/archiver": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
+ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "archiver-utils": "^2.1.0",
+ "async": "^3.2.4",
+ "buffer-crc32": "^0.2.1",
+ "readable-stream": "^3.6.0",
+ "readdir-glob": "^1.1.2",
+ "tar-stream": "^2.2.0",
+ "zip-stream": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/archiver-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
+ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.0",
+ "lazystream": "^1.0.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.difference": "^4.5.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.union": "^4.6.0",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/archiver-utils/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
+ "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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/assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/async-exit-hook": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz",
+ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.27",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
+ "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
+ "dev": true,
+ "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.1",
+ "caniuse-lite": "^1.0.30001774",
+ "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/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.9",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz",
+ "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bluebird-lst": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz",
+ "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bluebird": "^3.5.5"
+ }
+ },
+ "node_modules/boolean": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
+ "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/builder-util": {
+ "version": "25.1.7",
+ "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz",
+ "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.1.6",
+ "7zip-bin": "~5.2.0",
+ "app-builder-bin": "5.0.0-alpha.10",
+ "bluebird-lst": "^1.0.9",
+ "builder-util-runtime": "9.2.10",
+ "chalk": "^4.1.2",
+ "cross-spawn": "^7.0.3",
+ "debug": "^4.3.4",
+ "fs-extra": "^10.1.0",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.0",
+ "is-ci": "^3.0.0",
+ "js-yaml": "^4.1.0",
+ "source-map-support": "^0.5.19",
+ "stat-mode": "^1.0.0",
+ "temp-file": "^3.4.0"
+ }
+ },
+ "node_modules/builder-util-runtime": {
+ "version": "9.2.10",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz",
+ "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4",
+ "sax": "^1.2.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/builder-util/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/builder-util/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/builder-util/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/cacache": {
+ "version": "16.1.3",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz",
+ "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/fs": "^2.1.0",
+ "@npmcli/move-file": "^2.0.0",
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.1.0",
+ "glob": "^8.0.1",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^7.7.1",
+ "minipass": "^3.1.6",
+ "minipass-collect": "^1.0.2",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "mkdirp": "^1.0.4",
+ "p-map": "^4.0.0",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^3.0.2",
+ "ssri": "^9.0.0",
+ "tar": "^6.1.11",
+ "unique-filename": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/cacache/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cacache/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/cacache/node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/cacache/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cacache/node_modules/minimatch": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cacheable-lookup": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+ "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.6.0"
+ }
+ },
+ "node_modules/cacheable-request": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
+ "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^4.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^6.0.1",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001780",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz",
+ "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "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/chokidar/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==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chromium-pickle-js": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz",
+ "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ci-info": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+ "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+ "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "slice-ansi": "^3.0.0",
+ "string-width": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "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/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/clone-response": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
+ "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
+ "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/compare-version": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
+ "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/compress-commons": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
+ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "buffer-crc32": "^0.2.13",
+ "crc32-stream": "^4.0.2",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concurrently": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
+ "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "4.1.2",
+ "rxjs": "7.8.2",
+ "shell-quote": "1.8.3",
+ "supports-color": "8.1.1",
+ "tree-kill": "1.2.2",
+ "yargs": "17.7.2"
+ },
+ "bin": {
+ "conc": "dist/bin/concurrently.js",
+ "concurrently": "dist/bin/concurrently.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/config-file-ts": {
+ "version": "0.2.8-rc1",
+ "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz",
+ "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob": "^10.3.12",
+ "typescript": "^5.4.3"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/config-file-ts/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/config-file-ts/node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/crc": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
+ "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "buffer": "^5.1.0"
+ }
+ },
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/crc32-stream": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
+ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "readable-stream": "^3.4.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "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/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decompress-response/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/defer-to-connect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dir-compare": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
+ "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimatch": "^3.0.5",
+ "p-limit": "^3.1.0 "
+ }
+ },
+ "node_modules/dir-compare/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dir-compare/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/dir-compare/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dmg-builder": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz",
+ "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "app-builder-lib": "25.1.8",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "fs-extra": "^10.1.0",
+ "iconv-lite": "^0.6.2",
+ "js-yaml": "^4.1.0"
+ },
+ "optionalDependencies": {
+ "dmg-license": "^1.0.11"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/dmg-builder/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/dmg-license": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
+ "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "dependencies": {
+ "@types/plist": "^3.0.1",
+ "@types/verror": "^1.10.3",
+ "ajv": "^6.10.0",
+ "crc": "^3.8.0",
+ "iconv-corefoundation": "^1.1.7",
+ "plist": "^3.0.4",
+ "smart-buffer": "^4.0.2",
+ "verror": "^1.10.0"
+ },
+ "bin": {
+ "dmg-license": "bin/dmg-license.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dotenv-expand": {
+ "version": "11.0.7",
+ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",
+ "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dotenv": "^16.4.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/electron": {
+ "version": "33.4.11",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz",
+ "integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@electron/get": "^2.0.0",
+ "@types/node": "^20.9.0",
+ "extract-zip": "^2.0.1"
+ },
+ "bin": {
+ "electron": "cli.js"
+ },
+ "engines": {
+ "node": ">= 12.20.55"
+ }
+ },
+ "node_modules/electron-builder": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz",
+ "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "app-builder-lib": "25.1.8",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "chalk": "^4.1.2",
+ "dmg-builder": "25.1.8",
+ "fs-extra": "^10.1.0",
+ "is-ci": "^3.0.0",
+ "lazy-val": "^1.0.5",
+ "simple-update-notifier": "2.0.0",
+ "yargs": "^17.6.2"
+ },
+ "bin": {
+ "electron-builder": "cli.js",
+ "install-app-deps": "install-app-deps.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows": {
+ "version": "25.1.8",
+ "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz",
+ "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "app-builder-lib": "25.1.8",
+ "archiver": "^5.3.1",
+ "builder-util": "25.1.7",
+ "fs-extra": "^10.1.0"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-builder-squirrel-windows/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/electron-builder/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-builder/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-builder/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/electron-publish": {
+ "version": "25.1.7",
+ "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz",
+ "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/fs-extra": "^9.0.11",
+ "builder-util": "25.1.7",
+ "builder-util-runtime": "9.2.10",
+ "chalk": "^4.1.2",
+ "fs-extra": "^10.1.0",
+ "lazy-val": "^1.0.5",
+ "mime": "^2.5.2"
+ }
+ },
+ "node_modules/electron-publish/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-publish/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-publish/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.321",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz",
+ "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/electron-updater": {
+ "version": "6.8.9",
+ "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.9.tgz",
+ "integrity": "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==",
+ "license": "MIT",
+ "dependencies": {
+ "builder-util-runtime": "9.7.0",
+ "fs-extra": "^10.1.0",
+ "js-yaml": "^4.1.0",
+ "lazy-val": "^1.0.5",
+ "lodash.escaperegexp": "^4.1.2",
+ "lodash.isequal": "^4.5.0",
+ "semver": "~7.7.3",
+ "tiny-typed-emitter": "^2.1.0"
+ }
+ },
+ "node_modules/electron-updater/node_modules/builder-util-runtime": {
+ "version": "9.7.0",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz",
+ "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4",
+ "sax": "^1.2.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/electron-updater/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/electron-updater/node_modules/jsonfile": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-updater/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/electron-updater/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "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/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es6-error": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/exponential-backoff": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
+ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/extsprintf": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
+ "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
+ "dev": true,
+ "engines": [
+ "node >=0.6.0"
+ ],
+ "license": "MIT",
+ "optional": true
+ },
+ "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==",
+ "dev": true,
+ "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-glob/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==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/filelist": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
+ "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/filelist/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/filelist/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/filelist/node_modules/minimatch": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/foreground-child/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==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gauge": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
+ "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.3",
+ "console-control-strings": "^1.1.0",
+ "has-unicode": "^2.0.1",
+ "signal-exit": "^3.0.7",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/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-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/global-agent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
+ "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "es6-error": "^4.1.1",
+ "matcher": "^3.0.0",
+ "roarr": "^2.15.3",
+ "semver": "^7.3.2",
+ "serialize-error": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=10.0"
+ }
+ },
+ "node_modules/global-agent/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/got": {
+ "version": "11.8.6",
+ "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
+ "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/is": "^4.0.0",
+ "@szmarczak/http-timer": "^4.0.5",
+ "@types/cacheable-request": "^6.0.1",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^5.0.3",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "http2-wrapper": "^1.0.0-beta.5.2",
+ "lowercase-keys": "^2.0.0",
+ "p-cancelable": "^2.0.0",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
+ }
+ },
+ "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/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
+ "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/hosted-git-info/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/hosted-git-info/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "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==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/http2-wrapper": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
+ "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/iconv-corefoundation": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
+ "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "dependencies": {
+ "cli-truncate": "^2.1.0",
+ "node-addon-api": "^1.6.3"
+ },
+ "engines": {
+ "node": "^8.11.2 || >=10"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/infer-owner": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
+ "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ip-address": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-ci": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz",
+ "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ci-info": "^3.2.0"
+ },
+ "bin": {
+ "is-ci": "bin.js"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-lambda": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
+ "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/isbinaryfile": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz",
+ "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/gjtorikian/"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jake": {
+ "version": "10.9.4",
+ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz",
+ "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.6",
+ "filelist": "^1.0.4",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+ "dev": true,
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "license": "MIT",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/lazy-val": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
+ "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
+ "license": "MIT"
+ },
+ "node_modules/lazystream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "readable-stream": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.6.3"
+ }
+ },
+ "node_modules/lazystream/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/lazystream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lazystream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/linkify-it": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz",
+ "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.23",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.difference": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
+ "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.escaperegexp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
+ "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/lodash.union": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
+ "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lowercase-keys": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz",
+ "integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz",
+ "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "agentkeepalive": "^4.2.1",
+ "cacache": "^16.1.0",
+ "http-cache-semantics": "^4.1.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "is-lambda": "^1.0.1",
+ "lru-cache": "^7.7.1",
+ "minipass": "^3.1.6",
+ "minipass-collect": "^1.0.2",
+ "minipass-fetch": "^2.0.3",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^0.6.3",
+ "promise-retry": "^2.0.1",
+ "socks-proxy-agent": "^7.0.0",
+ "ssri": "^9.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/make-fetch-happen/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/markdown-it": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz",
+ "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.1",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/matcher": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
+ "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "escape-string-regexp": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
+ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz",
+ "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-fetch": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz",
+ "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.1.6",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^2.1.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.13"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+ "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "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/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.89.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz",
+ "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-abi/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
+ "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/node-api-version": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz",
+ "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ }
+ },
+ "node_modules/node-api-version/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-gyp": {
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz",
+ "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^10.0.3",
+ "nopt": "^6.0.0",
+ "npmlog": "^6.0.0",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.2",
+ "which": "^2.0.2"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": "^12.13 || ^14.13 || >=16"
+ }
+ },
+ "node_modules/node-gyp/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.36",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nopt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
+ "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^1.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-url": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npmlog": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz",
+ "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==",
+ "deprecated": "This package is no longer supported.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "are-we-there-yet": "^3.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^4.0.3",
+ "set-blocking": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-cancelable": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
+ "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
+ "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/path-scurry/node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/pe-library": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz",
+ "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jet2jet"
+ }
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/plist": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
+ "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xmldom/xmldom": "^0.8.8",
+ "base64-js": "^1.5.1",
+ "xmlbuilder": "^15.1.1"
+ },
+ "engines": {
+ "node": ">=10.4.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/promise-inflight": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
+ "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/read-binary-file-arch": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
+ "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "bin": {
+ "read-binary-file-arch": "cli.js"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdir-glob": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
+ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "minimatch": "^5.1.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/readdir-glob/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/minimatch": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "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/resedit": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz",
+ "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pe-library": "^0.4.1"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jet2jet"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-alpn": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/responselike": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
+ "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lowercase-keys": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/roarr": {
+ "version": "2.15.4",
+ "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
+ "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "dependencies": {
+ "boolean": "^3.0.1",
+ "detect-node": "^2.0.4",
+ "globalthis": "^1.0.1",
+ "json-stringify-safe": "^5.0.1",
+ "semver-compare": "^1.0.0",
+ "sprintf-js": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "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==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/sanitize-filename": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz",
+ "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==",
+ "dev": true,
+ "license": "WTFPL OR ISC",
+ "dependencies": {
+ "truncate-utf8-bytes": "^1.0.0"
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
+ "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=11.0.0"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/serialize-error": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "type-fest": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/simple-update-notifier/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+ "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
+ "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^10.0.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz",
+ "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^6.0.2",
+ "debug": "^4.3.3",
+ "socks": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/ssri": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz",
+ "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/stat-mode": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
+ "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "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/string-width-cjs": {
+ "name": "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/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/strip-ansi-cjs": {
+ "name": "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/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "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/sucrase/node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/sumchecker": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
+ "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
+ "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/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar/node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/temp-file": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
+ "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-exit-hook": "^2.0.1",
+ "fs-extra": "^10.0.0"
+ }
+ },
+ "node_modules/temp-file/node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/temp-file/node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/temp-file/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tiny-typed-emitter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
+ "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tmp": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+ "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/tmp-promise": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
+ "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tmp": "^0.2.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/truncate-utf8-bytes": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
+ "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
+ "dev": true,
+ "license": "WTFPL",
+ "dependencies": {
+ "utf8-byte-length": "^1.0.1"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/type-fest": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unique-filename": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz",
+ "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "unique-slug": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/unique-slug": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz",
+ "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/utf8-byte-length": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
+ "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==",
+ "dev": true,
+ "license": "(WTFPL OR MIT)"
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/verror": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
+ "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "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/wrap-ansi-cjs": {
+ "name": "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/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/xmlbuilder": {
+ "version": "15.1.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
+ "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "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==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zip-stream": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
+ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "archiver-utils": "^3.0.4",
+ "compress-commons": "^4.1.2",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/zip-stream/node_modules/archiver-utils": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
+ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "glob": "^7.2.3",
+ "graceful-fs": "^4.2.0",
+ "lazystream": "^1.0.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.difference": "^4.5.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.union": "^4.6.0",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.14",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
+ "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/desktop/package.json b/desktop/package.json
new file mode 100644
index 00000000..5b1eaff2
--- /dev/null
+++ b/desktop/package.json
@@ -0,0 +1,103 @@
+{
+ "name": "cowagent-desktop",
+ "version": "1.0.0",
+ "description": "CowAgent Desktop Client - AI Agent on your desktop",
+ "main": "dist/main/index.js",
+ "author": "CowAgent",
+ "license": "MIT",
+ "scripts": {
+ "dev": "npm run build && electron .",
+ "dev:hot": "concurrently \"npm run dev:renderer\" \"sleep 2 && npm run dev:main\"",
+ "dev:main": "tsc -p tsconfig.main.json && electron .",
+ "dev:renderer": "vite",
+ "build": "npm run build:renderer && npm run build:main",
+ "build:main": "tsc -p tsconfig.main.json",
+ "build:renderer": "vite build",
+ "dist": "npm run build && electron-builder",
+ "dist:mac": "npm run build && electron-builder --mac",
+ "dist:win": "npm run build && electron-builder --win"
+ },
+ "dependencies": {
+ "electron-updater": "^6.8.9",
+ "highlight.js": "^11.11.1",
+ "lucide-react": "^1.21.0",
+ "markdown-it": "^14.2.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0",
+ "zustand": "^5.0.14"
+ },
+ "devDependencies": {
+ "@types/markdown-it": "^14.1.2",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "autoprefixer": "^10.4.20",
+ "concurrently": "^9.1.0",
+ "electron": "^33.2.0",
+ "electron-builder": "^25.1.8",
+ "postcss": "^8.4.49",
+ "tailwindcss": "^3.4.15",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.3"
+ },
+ "build": {
+ "appId": "com.cowagent.desktop",
+ "productName": "CowAgent",
+ "directories": {
+ "output": "release"
+ },
+ "files": [
+ "dist/**/*",
+ "resources/**/*"
+ ],
+ "extraResources": [
+ {
+ "from": "build/dist/cowagent-backend",
+ "to": "backend/cowagent-backend"
+ },
+ {
+ "from": "resources",
+ "to": ".",
+ "filter": [
+ "icon.png"
+ ]
+ }
+ ],
+ "mac": {
+ "category": "public.app-category.productivity",
+ "icon": "resources/icon.icns",
+ "target": [
+ {
+ "target": "dmg",
+ "arch": [
+ "arm64",
+ "x64"
+ ]
+ }
+ ]
+ },
+ "win": {
+ "icon": "resources/icon.ico",
+ "target": [
+ {
+ "target": "nsis",
+ "arch": [
+ "x64"
+ ]
+ }
+ ]
+ },
+ "nsis": {
+ "oneClick": false,
+ "allowToChangeInstallationDirectory": true,
+ "createDesktopShortcut": true,
+ "createStartMenuShortcut": true
+ },
+ "publish": {
+ "provider": "github",
+ "owner": "zhayujie",
+ "repo": "chatgpt-on-wechat"
+ }
+ }
+}
diff --git a/desktop/postcss.config.js b/desktop/postcss.config.js
new file mode 100644
index 00000000..33ad091d
--- /dev/null
+++ b/desktop/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/desktop/resources/icon.icns b/desktop/resources/icon.icns
new file mode 100644
index 00000000..be56144e
Binary files /dev/null and b/desktop/resources/icon.icns differ
diff --git a/desktop/resources/icon.ico b/desktop/resources/icon.ico
new file mode 100644
index 00000000..71f45667
Binary files /dev/null and b/desktop/resources/icon.ico differ
diff --git a/desktop/resources/icon.png b/desktop/resources/icon.png
new file mode 100644
index 00000000..402d75d6
Binary files /dev/null and b/desktop/resources/icon.png differ
diff --git a/desktop/src/main/index.ts b/desktop/src/main/index.ts
new file mode 100644
index 00000000..36816f5e
--- /dev/null
+++ b/desktop/src/main/index.ts
@@ -0,0 +1,307 @@
+import { app, BrowserWindow, shell, ipcMain, dialog, nativeImage } from 'electron'
+import path from 'path'
+import fs from 'fs'
+import http from 'http'
+import { PythonBackend } from './python-manager'
+import { buildAppMenu } from './menu'
+import { createTray, destroyTray } from './tray'
+import { initUpdater, checkForUpdates, startDownload, quitAndInstall } from './updater'
+
+// Force the product name so the Dock/menu shows "CowAgent" even in dev mode,
+// where the default Electron binary would otherwise report "Electron".
+app.setName('CowAgent')
+
+let mainWindow: BrowserWindow | null = null
+let pythonBackend: PythonBackend | null = null
+// True once the user explicitly quits (menu/tray), so close-to-tray is bypassed.
+let isQuitting = false
+
+const isDev = !app.isPackaged
+const VITE_DEV_PORTS = [5173, 5174, 5175, 5176]
+
+function probePort(port: number): Promise {
+ return new Promise((resolve) => {
+ const req = http.get(`http://localhost:${port}`, (res) => {
+ resolve(res.statusCode !== undefined)
+ })
+ req.on('error', () => resolve(false))
+ req.setTimeout(500, () => { req.destroy(); resolve(false) })
+ })
+}
+
+async function findViteDevServer(): Promise {
+ for (const port of VITE_DEV_PORTS) {
+ if (await probePort(port)) {
+ return `http://localhost:${port}`
+ }
+ }
+ return null
+}
+
+function getIconPath(ext: string = 'png'): string | undefined {
+ const iconFile = `icon.${ext}`
+ const iconPath = isDev
+ ? path.resolve(__dirname, '../../resources', iconFile)
+ : path.join(process.resourcesPath, iconFile)
+ if (fs.existsSync(iconPath)) return iconPath
+ return undefined
+}
+
+const isMac = process.platform === 'darwin'
+const isWin = process.platform === 'win32'
+
+// Persisted window bounds
+const windowStateFile = () => path.join(app.getPath('userData'), 'window-state.json')
+
+function loadWindowState(): { width: number; height: number; x?: number; y?: number } {
+ try {
+ const raw = fs.readFileSync(windowStateFile(), 'utf-8')
+ const s = JSON.parse(raw)
+ if (typeof s.width === 'number' && typeof s.height === 'number') return s
+ } catch {
+ /* first run or unreadable */
+ }
+ return { width: 1280, height: 800 }
+}
+
+function saveWindowState() {
+ if (!mainWindow || mainWindow.isDestroyed()) return
+ if (mainWindow.isMinimized() || mainWindow.isFullScreen()) return
+ const b = mainWindow.getBounds()
+ try {
+ fs.writeFileSync(windowStateFile(), JSON.stringify(b))
+ } catch {
+ /* ignore */
+ }
+}
+
+function createWindow() {
+ const state = loadWindowState()
+
+ mainWindow = new BrowserWindow({
+ width: state.width,
+ height: state.height,
+ x: state.x,
+ y: state.y,
+ minWidth: 900,
+ minHeight: 600,
+ // macOS: native traffic lights inset into our custom titlebar.
+ // Windows: fully frameless; we render custom window controls in-app.
+ titleBarStyle: isMac ? 'hiddenInset' : 'hidden',
+ trafficLightPosition: isMac ? { x: 14, y: 16 } : undefined,
+ frame: isMac ? undefined : false,
+ backgroundColor: '#0e0e10',
+ icon: getIconPath(),
+ show: false,
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.js'),
+ contextIsolation: true,
+ nodeIntegration: false,
+ },
+ })
+
+ const persist = () => saveWindowState()
+ mainWindow.on('resize', persist)
+ mainWindow.on('move', persist)
+ mainWindow.on('maximize', emitMaximizeState)
+ mainWindow.on('unmaximize', emitMaximizeState)
+
+ const rendererHtml = path.join(__dirname, '../renderer/index.html')
+
+ if (isDev) {
+ findViteDevServer().then((devUrl) => {
+ if (devUrl) {
+ console.log(`[Electron] Loading Vite dev server: ${devUrl}`)
+ mainWindow?.loadURL(devUrl)
+ mainWindow?.webContents.openDevTools()
+ } else if (fs.existsSync(rendererHtml)) {
+ console.log('[Electron] Vite dev server not found, loading built files')
+ mainWindow?.loadFile(rendererHtml)
+ } else {
+ console.error('[Electron] No renderer available. Run "npm run build:renderer" first.')
+ }
+ })
+ } else {
+ mainWindow.loadFile(rendererHtml)
+ }
+
+ mainWindow.once('ready-to-show', () => {
+ mainWindow?.show()
+ })
+
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
+ shell.openExternal(url)
+ return { action: 'deny' }
+ })
+
+ // Close-to-tray: hide the window instead of destroying it, so the tray's
+ // "Show" can bring it back. Only a real Quit (menu/tray/Cmd+Q) destroys it.
+ mainWindow.on('close', (e) => {
+ if (!isQuitting) {
+ e.preventDefault()
+ mainWindow?.hide()
+ }
+ })
+
+ mainWindow.on('closed', () => {
+ mainWindow = null
+ })
+}
+
+function getBackendPath(): string {
+ if (isDev) {
+ return path.resolve(__dirname, '../../..')
+ }
+ return path.join(process.resourcesPath, 'backend')
+}
+
+async function startBackend() {
+ const backendPath = getBackendPath()
+ pythonBackend = new PythonBackend(backendPath)
+
+ pythonBackend.on('ready', (port: number) => {
+ mainWindow?.webContents.send('backend-status', { status: 'ready', port })
+ })
+
+ pythonBackend.on('error', (error: string) => {
+ mainWindow?.webContents.send('backend-status', { status: 'error', error })
+ })
+
+ pythonBackend.on('log', (line: string) => {
+ mainWindow?.webContents.send('backend-log', line)
+ })
+
+ await pythonBackend.start()
+}
+
+function setupIPC() {
+ ipcMain.handle('get-backend-port', () => {
+ return pythonBackend?.getPort() ?? null
+ })
+
+ ipcMain.handle('get-backend-status', () => {
+ return pythonBackend?.getStatus() ?? 'stopped'
+ })
+
+ ipcMain.handle('restart-backend', async () => {
+ await pythonBackend?.restart()
+ return true
+ })
+
+ ipcMain.handle('select-directory', async () => {
+ const result = await dialog.showOpenDialog({
+ properties: ['openDirectory'],
+ })
+ return result.canceled ? null : result.filePaths[0]
+ })
+
+ ipcMain.handle('select-file', async (_event, filters?: Electron.FileFilter[]) => {
+ const result = await dialog.showOpenDialog({
+ properties: ['openFile'],
+ filters: filters || [{ name: 'All Files', extensions: ['*'] }],
+ })
+ return result.canceled ? null : result.filePaths[0]
+ })
+
+ // Custom window controls (used by Windows frameless titlebar)
+ ipcMain.handle('window-minimize', () => mainWindow?.minimize())
+ ipcMain.handle('window-maximize', () => {
+ if (!mainWindow) return false
+ if (mainWindow.isMaximized()) mainWindow.unmaximize()
+ else mainWindow.maximize()
+ return mainWindow.isMaximized()
+ })
+ ipcMain.handle('window-close', () => mainWindow?.close())
+ ipcMain.handle('window-is-maximized', () => mainWindow?.isMaximized() ?? false)
+
+ // Auto-update controls (renderer-driven: check, then opt-in download/install)
+ ipcMain.handle('update-check', () => checkForUpdates())
+ ipcMain.handle('update-download', () => startDownload())
+ ipcMain.handle('update-install', () => quitAndInstall())
+
+ // Synchronous OS locale lookup (e.g. "zh-CN", "en-US"). Used by the renderer
+ // to pick a sensible default UI language on first run before any paint.
+ ipcMain.on('get-system-locale', (event) => {
+ event.returnValue = app.getLocale() || app.getSystemLocale?.() || ''
+ })
+}
+
+function emitMaximizeState() {
+ const max = mainWindow?.isMaximized() ?? false
+ mainWindow?.webContents.send('window-maximize-changed', max)
+}
+
+// Single-instance lock: focus the existing window instead of opening a second app.
+const gotTheLock = app.requestSingleInstanceLock()
+if (!gotTheLock) {
+ app.quit()
+} else {
+ app.on('second-instance', () => {
+ if (mainWindow) {
+ if (mainWindow.isMinimized()) mainWindow.restore()
+ mainWindow.show()
+ mainWindow.focus()
+ }
+ })
+}
+
+app.whenReady().then(async () => {
+ // Set Dock icon on macOS (PNG is most reliable for nativeImage)
+ if (process.platform === 'darwin') {
+ const pngPath = getIconPath('png')
+ if (pngPath) {
+ const icon = nativeImage.createFromPath(pngPath)
+ if (!icon.isEmpty()) {
+ app.dock.setIcon(icon)
+ console.log('[Electron] Dock icon set:', pngPath)
+ } else {
+ console.warn('[Electron] Dock icon loaded but empty:', pngPath)
+ }
+ } else {
+ console.warn('[Electron] Dock icon not found in resources/')
+ }
+ }
+
+ setupIPC()
+ createWindow()
+ buildAppMenu(() => mainWindow)
+ // No menu-bar tray on macOS — the Dock + window controls are enough there.
+ // Keep the tray on Windows/Linux where minimizing to a tray icon is expected.
+ if (!isMac) {
+ createTray({
+ getWindow: () => mainWindow,
+ iconPath: getIconPath('png'),
+ onQuit: () => {
+ isQuitting = true
+ app.quit()
+ },
+ })
+ }
+ await startBackend()
+
+ // Wire auto-update and do a first silent check a few seconds after launch so
+ // it doesn't compete with backend startup for resources.
+ initUpdater(() => mainWindow)
+ setTimeout(() => checkForUpdates(), 5000)
+
+ app.on('activate', () => {
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createWindow()
+ } else {
+ mainWindow?.show()
+ }
+ })
+})
+
+app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') {
+ app.quit()
+ }
+})
+
+app.on('before-quit', () => {
+ isQuitting = true
+ saveWindowState()
+ destroyTray()
+ pythonBackend?.stop()
+})
diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts
new file mode 100644
index 00000000..d5195a3f
--- /dev/null
+++ b/desktop/src/main/menu.ts
@@ -0,0 +1,112 @@
+import { app, Menu, BrowserWindow, shell } from 'electron'
+import type { MenuItemConstructorOptions } from 'electron'
+
+const isMac = process.platform === 'darwin'
+const SKILL_HUB_URL = 'https://skills.cowagent.ai/'
+const DOCS_URL = 'https://docs.cowagent.ai'
+
+// Send a menu-triggered action to the renderer (e.g. new chat, open settings).
+function emit(win: BrowserWindow | null, action: string) {
+ win?.webContents.send('menu-action', action)
+}
+
+/**
+ * Build a minimal, purpose-built application menu. We intentionally drop most of
+ * Electron's verbose defaults and keep only items that are actually useful for
+ * this app, plus the shortcuts users expect (New Chat, Settings, Reload, etc).
+ */
+export function buildAppMenu(getWindow: () => BrowserWindow | null) {
+ const win = () => getWindow()
+
+ const appMenu: MenuItemConstructorOptions[] = isMac
+ ? [
+ {
+ label: app.name,
+ submenu: [
+ { role: 'about' },
+ { type: 'separator' },
+ { label: 'Settings…', accelerator: 'Cmd+,', click: () => emit(win(), 'open-settings') },
+ { type: 'separator' },
+ { role: 'hide' },
+ { role: 'hideOthers' },
+ { role: 'unhide' },
+ { type: 'separator' },
+ { role: 'quit' },
+ ],
+ },
+ ]
+ : []
+
+ const fileMenu: MenuItemConstructorOptions = {
+ label: 'File',
+ submenu: [
+ { label: 'New Chat', accelerator: 'CmdOrCtrl+N', click: () => emit(win(), 'new-chat') },
+ ...(!isMac
+ ? ([
+ { label: 'Settings', accelerator: 'Ctrl+,', click: () => emit(win(), 'open-settings') },
+ { type: 'separator' },
+ { role: 'quit' },
+ ] as MenuItemConstructorOptions[])
+ : []),
+ ],
+ }
+
+ const editMenu: MenuItemConstructorOptions = {
+ label: 'Edit',
+ submenu: [
+ { role: 'undo' },
+ { role: 'redo' },
+ { type: 'separator' },
+ { role: 'cut' },
+ { role: 'copy' },
+ { role: 'paste' },
+ { role: 'selectAll' },
+ ],
+ }
+
+ const viewMenu: MenuItemConstructorOptions = {
+ label: 'View',
+ submenu: [
+ { role: 'reload' },
+ { role: 'toggleDevTools' },
+ { type: 'separator' },
+ { role: 'resetZoom' },
+ { role: 'zoomIn' },
+ { role: 'zoomOut' },
+ { type: 'separator' },
+ { role: 'togglefullscreen' },
+ ],
+ }
+
+ const windowMenu: MenuItemConstructorOptions = {
+ label: 'Window',
+ submenu: [
+ { role: 'minimize' },
+ ...(isMac ? ([{ role: 'zoom' }] as MenuItemConstructorOptions[]) : []),
+ { type: 'separator' },
+ // Explicit Close so Cmd/Ctrl+W reliably triggers our close-to-tray hide.
+ { label: 'Close Window', accelerator: 'CmdOrCtrl+W', click: () => win()?.close() },
+ ],
+ }
+
+ const helpMenu: MenuItemConstructorOptions = {
+ label: 'Help',
+ submenu: [
+ { label: 'View Logs', click: () => emit(win(), 'view-logs') },
+ { type: 'separator' },
+ { label: 'Documentation', click: () => shell.openExternal(DOCS_URL) },
+ { label: 'Skill Hub', click: () => shell.openExternal(SKILL_HUB_URL) },
+ ],
+ }
+
+ const template: MenuItemConstructorOptions[] = [
+ ...appMenu,
+ fileMenu,
+ editMenu,
+ viewMenu,
+ windowMenu,
+ helpMenu,
+ ]
+
+ Menu.setApplicationMenu(Menu.buildFromTemplate(template))
+}
diff --git a/desktop/src/main/preload.ts b/desktop/src/main/preload.ts
new file mode 100644
index 00000000..3e7de874
--- /dev/null
+++ b/desktop/src/main/preload.ts
@@ -0,0 +1,62 @@
+import { contextBridge, ipcRenderer } from 'electron'
+
+contextBridge.exposeInMainWorld('electronAPI', {
+ getBackendPort: () => ipcRenderer.invoke('get-backend-port'),
+ getBackendStatus: () => ipcRenderer.invoke('get-backend-status'),
+ restartBackend: () => ipcRenderer.invoke('restart-backend'),
+ selectDirectory: () => ipcRenderer.invoke('select-directory'),
+ selectFile: (filters?: Electron.FileFilter[]) => ipcRenderer.invoke('select-file', filters),
+
+ // Each listener registrar returns an unsubscribe fn so renderers can clean
+ // up on unmount / effect re-run and avoid accumulating duplicate handlers.
+ onBackendStatus: (callback: (data: { status: string; port?: number; error?: string }) => void) => {
+ const handler = (_event: unknown, data: { status: string; port?: number; error?: string }) => callback(data)
+ ipcRenderer.on('backend-status', handler)
+ return () => ipcRenderer.removeListener('backend-status', handler)
+ },
+
+ onBackendLog: (callback: (line: string) => void) => {
+ const handler = (_event: unknown, line: string) => callback(line)
+ ipcRenderer.on('backend-log', handler)
+ return () => ipcRenderer.removeListener('backend-log', handler)
+ },
+
+ // Window controls (custom titlebar on Windows)
+ windowMinimize: () => ipcRenderer.invoke('window-minimize'),
+ windowMaximize: () => ipcRenderer.invoke('window-maximize'),
+ windowClose: () => ipcRenderer.invoke('window-close'),
+ windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
+ onMaximizeChange: (callback: (maximized: boolean) => void) => {
+ const handler = (_event: unknown, max: boolean) => callback(max)
+ ipcRenderer.on('window-maximize-changed', handler)
+ return () => ipcRenderer.removeListener('window-maximize-changed', handler)
+ },
+
+ // App menu / shortcut actions forwarded from the main process.
+ onMenuAction: (callback: (action: string) => void) => {
+ const handler = (_event: unknown, action: string) => callback(action)
+ ipcRenderer.on('menu-action', handler)
+ return () => ipcRenderer.removeListener('menu-action', handler)
+ },
+
+ // Auto-update: trigger checks/download/install and subscribe to status.
+ checkForUpdate: () => ipcRenderer.invoke('update-check'),
+ downloadUpdate: () => ipcRenderer.invoke('update-download'),
+ installUpdate: () => ipcRenderer.invoke('update-install'),
+ onUpdateStatus: (callback: (status: unknown) => void) => {
+ const handler = (_event: unknown, status: unknown) => callback(status)
+ ipcRenderer.on('update-status', handler)
+ return () => ipcRenderer.removeListener('update-status', handler)
+ },
+
+ platform: process.platform,
+ // OS UI language (e.g. "zh-CN"), read synchronously so the renderer can pick
+ // a default language on first run. Falls back to '' if unavailable.
+ systemLocale: (() => {
+ try {
+ return ipcRenderer.sendSync('get-system-locale') as string
+ } catch {
+ return ''
+ }
+ })(),
+})
diff --git a/desktop/src/main/python-manager.ts b/desktop/src/main/python-manager.ts
new file mode 100644
index 00000000..33857f20
--- /dev/null
+++ b/desktop/src/main/python-manager.ts
@@ -0,0 +1,255 @@
+import { ChildProcess, spawn } from 'child_process'
+import { EventEmitter } from 'events'
+import path from 'path'
+import os from 'os'
+import fs from 'fs'
+import http from 'http'
+
+// Writable data dir for the packaged app (config.json, run.log, user data).
+// Lives in the user's home so it survives app updates and avoids writing into
+// the read-only app bundle. Source/dev runs keep using the repo CWD instead.
+const COW_DATA_DIR = path.join(os.homedir(), '.cow')
+
+export class PythonBackend extends EventEmitter {
+ private process: ChildProcess | null = null
+ private backendPath: string
+ private port: number = 9899
+ private status: 'stopped' | 'starting' | 'ready' | 'error' = 'stopped'
+
+ constructor(backendPath: string) {
+ super()
+ this.backendPath = backendPath
+ }
+
+ getPort(): number {
+ return this.port
+ }
+
+ getStatus(): string {
+ return this.status
+ }
+
+ /**
+ * Locate the packaged onedir backend executable shipped with the app.
+ * Returns null when not present (e.g. during local development), so we can
+ * fall back to running app.py with a system/venv Python.
+ */
+ private findBundledBackend(): string | null {
+ const exeName = process.platform === 'win32' ? 'cowagent-backend.exe' : 'cowagent-backend'
+ const candidates = [
+ path.join(this.backendPath, 'cowagent-backend', exeName),
+ path.join(this.backendPath, exeName),
+ ]
+ for (const p of candidates) {
+ if (fs.existsSync(p)) {
+ return p
+ }
+ }
+ return null
+ }
+
+ private findPython(): string {
+ const venvPaths = [
+ path.join(this.backendPath, '.venv', 'bin', 'python'),
+ path.join(this.backendPath, '.venv', 'Scripts', 'python.exe'),
+ path.join(this.backendPath, 'venv', 'bin', 'python'),
+ path.join(this.backendPath, 'venv', 'Scripts', 'python.exe'),
+ ]
+
+ for (const p of venvPaths) {
+ if (fs.existsSync(p)) {
+ return p
+ }
+ }
+
+ return process.platform === 'win32' ? 'python' : 'python3'
+ }
+
+ /**
+ * Resolve config.json from the given data dir to read the web port. The
+ * packaged build keeps config in COW_DATA_DIR (~/.cow); dev reads it from the
+ * repo path. Returns the default port when no config (or web_port) is found.
+ */
+ private readPort(dataDir: string): number {
+ try {
+ const configPath = path.join(dataDir, 'config.json')
+ if (fs.existsSync(configPath)) {
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
+ if (config.web_port) {
+ return config.web_port
+ }
+ }
+ } catch {
+ // ignore
+ }
+ return 9899
+ }
+
+ async start(): Promise {
+ if (this.status === 'ready' || this.status === 'starting') {
+ return
+ }
+
+ this.status = 'starting'
+
+ // Prefer the packaged self-contained backend (production); fall back to
+ // running app.py with a Python interpreter (local development).
+ const bundled = this.findBundledBackend()
+ // Packaged app stores writable data in ~/.cow; dev keeps it in the repo.
+ const dataDir = bundled ? COW_DATA_DIR : this.backendPath
+ this.port = this.readPort(dataDir)
+
+ const alreadyRunning = await this.probeHealth()
+ if (alreadyRunning) {
+ this.status = 'ready'
+ this.emit('log', `Backend already running on port ${this.port}`)
+ this.emit('ready', this.port)
+ return
+ }
+
+ let command: string
+ let args: string[]
+ let cwd: string
+
+ if (bundled) {
+ command = bundled
+ args = []
+ // The onedir bundle reads data files relative to the executable's dir.
+ cwd = path.dirname(bundled)
+ this.emit('log', `Starting bundled backend: ${bundled}`)
+ } else {
+ const pythonPath = this.findPython()
+ const appPath = path.join(this.backendPath, 'app.py')
+ if (!fs.existsSync(appPath)) {
+ this.status = 'error'
+ this.emit('error', `app.py not found at ${appPath}`)
+ return
+ }
+ command = pythonPath
+ args = [appPath]
+ cwd = this.backendPath
+ this.emit('log', `Starting Python backend: ${pythonPath} ${appPath}`)
+ }
+
+ this.process = spawn(command, args, {
+ cwd,
+ // COW_DESKTOP enables the lighter desktop runtime (no plugins, no MCP).
+ // COW_DATA_DIR (packaged only) redirects writable data to ~/.cow so the
+ // app bundle stays read-only; dev runs omit it and keep using the repo.
+ env: {
+ ...process.env,
+ PYTHONUNBUFFERED: '1',
+ COW_DESKTOP: '1',
+ ...(bundled ? { COW_DATA_DIR } : {}),
+ },
+ stdio: ['pipe', 'pipe', 'pipe'],
+ })
+
+ this.process.stdout?.on('data', (data: Buffer) => {
+ const lines = data.toString().split('\n').filter(Boolean)
+ for (const line of lines) {
+ this.emit('log', line)
+ }
+ })
+
+ this.process.stderr?.on('data', (data: Buffer) => {
+ const lines = data.toString().split('\n').filter(Boolean)
+ for (const line of lines) {
+ this.emit('log', line)
+ }
+ })
+
+ this.process.on('exit', (code) => {
+ this.status = 'stopped'
+ this.emit('log', `Python process exited with code ${code}`)
+ if (code !== 0 && code !== null) {
+ this.emit('error', `Python process exited with code ${code}`)
+ }
+ })
+
+ this.process.on('error', (err) => {
+ this.status = 'error'
+ this.emit('error', `Failed to start Python: ${err.message}`)
+ })
+
+ await this.waitForReady()
+ }
+
+ private probeHealth(): Promise {
+ return new Promise((resolve) => {
+ const req = http.get(`http://127.0.0.1:${this.port}/config`, (res) => {
+ resolve(res.statusCode === 200)
+ })
+ req.on('error', () => resolve(false))
+ req.setTimeout(2000, () => { req.destroy(); resolve(false) })
+ })
+ }
+
+ private waitForReady(): Promise {
+ return new Promise((resolve) => {
+ // Wall-clock deadline rather than an attempt counter: if the machine
+ // sleeps/suspends, the 1s timers stretch out and a counter would give up
+ // far too early. Time-based bounding tracks real elapsed time instead.
+ const timeoutMs = 120_000
+ const startedAt = Date.now()
+
+ const check = () => {
+ const req = http.get(`http://127.0.0.1:${this.port}/config`, (res) => {
+ if (res.statusCode === 200) {
+ this.status = 'ready'
+ this.emit('log', `Backend ready on port ${this.port}`)
+ this.emit('ready', this.port)
+ resolve()
+ } else {
+ retry()
+ }
+ })
+
+ req.on('error', () => retry())
+ req.setTimeout(2000, () => {
+ req.destroy()
+ retry()
+ })
+ }
+
+ const retry = () => {
+ if (this.status === 'stopped' || this.status === 'ready') {
+ resolve()
+ return
+ }
+ if (Date.now() - startedAt >= timeoutMs) {
+ this.status = 'error'
+ this.emit('error', `Backend failed to start within ${Math.round(timeoutMs / 1000)} seconds`)
+ resolve()
+ return
+ }
+ setTimeout(check, 1000)
+ }
+
+ setTimeout(check, 2000)
+ })
+ }
+
+ stop(): void {
+ const proc = this.process
+ if (proc) {
+ proc.kill('SIGTERM')
+ // Keep a local ref so the SIGKILL fallback can still reach the process
+ // even after we clear `this.process`; otherwise a stuck backend would
+ // never be force-killed and leak as a zombie.
+ setTimeout(() => {
+ if (!proc.killed) {
+ proc.kill('SIGKILL')
+ }
+ }, 5000)
+ this.process = null
+ }
+ this.status = 'stopped'
+ }
+
+ async restart(): Promise {
+ this.stop()
+ await new Promise((resolve) => setTimeout(resolve, 2000))
+ await this.start()
+ }
+}
diff --git a/desktop/src/main/tray.ts b/desktop/src/main/tray.ts
new file mode 100644
index 00000000..a26818ac
--- /dev/null
+++ b/desktop/src/main/tray.ts
@@ -0,0 +1,59 @@
+import { app, Tray, Menu, BrowserWindow, nativeImage } from 'electron'
+
+let tray: Tray | null = null
+
+interface TrayDeps {
+ getWindow: () => BrowserWindow | null
+ // Colored icon used on Windows/Linux trays.
+ iconPath?: string
+ // Called when the user picks "Quit" so the app can fully exit.
+ onQuit: () => void
+}
+
+// Build a system tray icon with a minimal menu (Windows/Linux only — macOS
+// uses the Dock instead). Lets users restore the window after closing it to the
+// background and start a new chat quickly.
+export function createTray({ getWindow, iconPath, onQuit }: TrayDeps): Tray | null {
+ if (tray) return tray
+ if (!iconPath) return null
+
+ let image = nativeImage.createFromPath(iconPath)
+ if (image.isEmpty()) return null
+ // Tray icons render small; resize to avoid an oversized image on some platforms.
+ image = image.resize({ width: 18, height: 18 })
+
+ tray = new Tray(image)
+ tray.setToolTip(app.name)
+
+ const showWindow = () => {
+ const win = getWindow()
+ if (!win) return
+ if (win.isMinimized()) win.restore()
+ win.show()
+ win.focus()
+ }
+
+ const contextMenu = Menu.buildFromTemplate([
+ { label: 'Show CowAgent', click: showWindow },
+ {
+ label: 'New Chat',
+ click: () => {
+ showWindow()
+ getWindow()?.webContents.send('menu-action', 'new-chat')
+ },
+ },
+ { type: 'separator' },
+ { label: 'Quit', click: onQuit },
+ ])
+ tray.setContextMenu(contextMenu)
+
+ // Single click restores the window (common Windows/Linux behavior).
+ tray.on('click', showWindow)
+
+ return tray
+}
+
+export function destroyTray() {
+ tray?.destroy()
+ tray = null
+}
diff --git a/desktop/src/main/updater.ts b/desktop/src/main/updater.ts
new file mode 100644
index 00000000..aec52736
--- /dev/null
+++ b/desktop/src/main/updater.ts
@@ -0,0 +1,73 @@
+import { app, BrowserWindow } from 'electron'
+// electron-updater is CommonJS: its members live on module.exports, with no
+// meaningful default export. Under module=commonjs + esModuleInterop, a named
+// import compiles to `electron_updater_1.autoUpdater` and resolves correctly,
+// whereas `import pkg from 'electron-updater'` yields undefined.
+import { autoUpdater } from 'electron-updater'
+
+// Status payloads pushed to the renderer over the 'update-status' channel.
+// The renderer drives the NavRail badge + update panel from these.
+export type UpdateStatus =
+ | { state: 'checking' }
+ | { state: 'available'; version: string; notes?: string }
+ | { state: 'not-available' }
+ | { state: 'downloading'; percent: number }
+ | { state: 'downloaded'; version: string }
+ | { state: 'error'; message: string }
+
+let getWindow: () => BrowserWindow | null = () => null
+
+function send(status: UpdateStatus) {
+ getWindow()?.webContents.send('update-status', status)
+}
+
+export function initUpdater(windowGetter: () => BrowserWindow | null): void {
+ getWindow = windowGetter
+
+ // In dev (not packaged) there's no update feed; skip wiring entirely so
+ // electron-updater doesn't throw on the missing app-update.yml.
+ if (!app.isPackaged) {
+ return
+ }
+
+ // User-driven flow: we surface "available" and let the user opt in to the
+ // download, rather than pulling bytes silently in the background.
+ autoUpdater.autoDownload = false
+ autoUpdater.autoInstallOnAppQuit = true
+
+ autoUpdater.on('checking-for-update', () => send({ state: 'checking' }))
+ autoUpdater.on('update-available', (info) =>
+ send({ state: 'available', version: info.version, notes: typeof info.releaseNotes === 'string' ? info.releaseNotes : undefined })
+ )
+ autoUpdater.on('update-not-available', () => send({ state: 'not-available' }))
+ autoUpdater.on('download-progress', (p) =>
+ send({ state: 'downloading', percent: Math.round(p.percent) })
+ )
+ autoUpdater.on('update-downloaded', (info) =>
+ send({ state: 'downloaded', version: info.version })
+ )
+ autoUpdater.on('error', (err) =>
+ send({ state: 'error', message: err == null ? 'unknown' : (err.message || String(err)) })
+ )
+}
+
+// Silent check shortly after launch; safe to call when not packaged (no-op).
+export function checkForUpdates(): void {
+ if (!app.isPackaged) return
+ autoUpdater.checkForUpdates().catch((err) => {
+ send({ state: 'error', message: err?.message || String(err) })
+ })
+}
+
+export function startDownload(): void {
+ if (!app.isPackaged) return
+ autoUpdater.downloadUpdate().catch((err) => {
+ send({ state: 'error', message: err?.message || String(err) })
+ })
+}
+
+export function quitAndInstall(): void {
+ if (!app.isPackaged) return
+ // isSilent=false (show installer), isForceRunAfter=true (relaunch after).
+ autoUpdater.quitAndInstall(false, true)
+}
diff --git a/desktop/src/renderer/index.html b/desktop/src/renderer/index.html
new file mode 100644
index 00000000..183267de
--- /dev/null
+++ b/desktop/src/renderer/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ CowAgent
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/src/renderer/src/App.tsx b/desktop/src/renderer/src/App.tsx
new file mode 100644
index 00000000..e479924e
--- /dev/null
+++ b/desktop/src/renderer/src/App.tsx
@@ -0,0 +1,140 @@
+import React, { useState, useCallback, useEffect } from 'react'
+import { Routes, Route, useLocation, useNavigate } from 'react-router-dom'
+import { PanelLeftOpen } from 'lucide-react'
+import NavRail from './layout/NavRail'
+import SessionList from './layout/SessionList'
+import WindowControls from './layout/WindowControls'
+import StatusScreen from './components/StatusScreen'
+import { useBackend } from './hooks/useBackend'
+import { usePlatform } from './hooks/usePlatform'
+import { useUIStore } from './store/uiStore'
+import { useSessionStore } from './store/sessionStore'
+import { initUpdateListener } from './store/updateStore'
+import { useOnboardingStore } from './store/onboardingStore'
+import OnboardingWizard from './components/OnboardingWizard'
+import apiClient from './api/client'
+import { t } from './i18n'
+import ChatPage from './pages/ChatPage'
+import SettingsPage from './pages/SettingsPage'
+import KnowledgePage from './pages/KnowledgePage'
+import SkillsPage from './pages/SkillsPage'
+import MemoryPage from './pages/MemoryPage'
+import ChannelsPage from './pages/ChannelsPage'
+import TasksPage from './pages/TasksPage'
+import LogsPage from './pages/LogsPage'
+
+const App: React.FC = () => {
+ const backend = useBackend()
+ const location = useLocation()
+ const navigate = useNavigate()
+ const { isWin } = usePlatform()
+ const { sessionsCollapsed, toggleSessions } = useUIStore()
+ const onboardingOpen = useOnboardingStore((s) => s.open)
+ const maybeOpenOnboarding = useOnboardingStore((s) => s.maybeOpen)
+ const [, forceUpdate] = useState(0)
+
+ useEffect(() => {
+ if (backend.status === 'ready') apiClient.setBaseUrl(backend.baseUrl)
+ }, [backend.status, backend.baseUrl])
+
+ // First-run check: once the backend is ready, decide whether to show the
+ // onboarding wizard. It's config-driven — shown whenever the chat model isn't
+ // configured (and not dismissed earlier this session); no persisted flag.
+ useEffect(() => {
+ if (backend.status !== 'ready') return
+ let cancelled = false
+ apiClient
+ .getModels()
+ .then((data) => {
+ if (cancelled) return
+ const chat = data.capabilities?.chat
+ // "Configured" needs a chat provider+model AND that provider's API key
+ // set. A default config can ship a model name with no key, which
+ // shouldn't count as ready — otherwise we'd skip onboarding for users
+ // who still need to enter a key.
+ const providerId = chat?.current_provider
+ const provider = data.providers?.find((p) => p.id === providerId)
+ const keyReady = !!provider && (provider.configured || (provider.is_custom && !!provider.custom_name))
+ const configured = !!providerId && !!chat?.current_model && keyReady
+ maybeOpenOnboarding(configured)
+ })
+ .catch(() => {
+ // If models can't be loaded, fall back to the flag-only decision.
+ if (!cancelled) maybeOpenOnboarding(false)
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [backend.status, maybeOpenOnboarding])
+
+ // Subscribe to auto-update status from the main process (no-op in dev).
+ useEffect(() => initUpdateListener(), [])
+
+ // Handle app-menu / shortcut actions forwarded from the main process.
+ useEffect(() => {
+ const off = window.electronAPI?.onMenuAction?.((action) => {
+ if (action === 'new-chat') {
+ useSessionStore.getState().newSession()
+ navigate('/')
+ } else if (action === 'open-settings') {
+ navigate('/settings')
+ } else if (action === 'view-logs') {
+ navigate('/logs')
+ }
+ })
+ return off
+ }, [navigate])
+
+ const handleLangChange = useCallback(() => forceUpdate((n) => n + 1), [])
+
+ if (backend.status !== 'ready') {
+ return
+ }
+
+ const isChat = location.pathname === '/'
+ const showSessions = isChat && !sessionsCollapsed
+
+ return (
+
+ {onboardingOpen &&
}
+
+
+ {showSessions &&
}
+
+
+ {/* Top titlebar strip — drag region + Windows controls */}
+
+ {isChat && sessionsCollapsed && (
+
+
+
+ )}
+
+ {isWin && }
+
+
+ {/* Content */}
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ {/* Legacy /models route now lives as a tab inside settings */}
+ } />
+ } />
+
+
+
+
+ )
+}
+
+export default App
diff --git a/desktop/src/renderer/src/api/client.ts b/desktop/src/renderer/src/api/client.ts
new file mode 100644
index 00000000..eb47db61
--- /dev/null
+++ b/desktop/src/renderer/src/api/client.ts
@@ -0,0 +1,404 @@
+import type {
+ ConfigData,
+ ChannelInfo,
+ ChannelAction,
+ SkillInfo,
+ ToolInfo,
+ MemoryItem,
+ MemoryCategory,
+ MemoryPage,
+ SchedulerTask,
+ Attachment,
+ SessionsPage,
+ HistoryPage,
+ ModelsData,
+ ModelsAction,
+ KnowledgeList,
+ KnowledgeGraph,
+ KnowledgeAction,
+} from '../types'
+
+interface ApiResult {
+ status: string
+ message?: string
+}
+
+class ApiClient {
+ private baseUrl = 'http://127.0.0.1:9899'
+
+ setBaseUrl(url: string) {
+ this.baseUrl = url
+ }
+
+ getBaseUrl() {
+ return this.baseUrl
+ }
+
+ private async request(path: string, options?: RequestInit): Promise {
+ const res = await fetch(`${this.baseUrl}${path}`, {
+ ...options,
+ // Send cookies for future web_password auth support
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers,
+ },
+ })
+ if (!res.ok) {
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`)
+ }
+ return res.json()
+ }
+
+ // ---------------------------------------------------------
+ // Chat / messages
+ // ---------------------------------------------------------
+
+ async sendMessage(
+ sessionId: string,
+ message: string,
+ opts?: { stream?: boolean; attachments?: Attachment[]; isVoice?: boolean; lang?: string }
+ ): Promise<{ status: string; request_id: string; stream: boolean; inline_reply?: string }> {
+ return this.request('/message', {
+ method: 'POST',
+ body: JSON.stringify({
+ session_id: sessionId,
+ message,
+ stream: opts?.stream ?? true,
+ attachments: opts?.attachments,
+ is_voice: opts?.isVoice ?? false,
+ lang: opts?.lang,
+ }),
+ })
+ }
+
+ async poll(sessionId: string): Promise<{
+ status: string
+ has_content: boolean
+ content?: string
+ request_id?: string
+ timestamp?: number
+ }> {
+ return this.request('/poll', {
+ method: 'POST',
+ body: JSON.stringify({ session_id: sessionId }),
+ })
+ }
+
+ async cancel(opts: { requestId?: string; sessionId?: string; lang?: string }): Promise<{ status: string; cancelled: number }> {
+ return this.request('/cancel', {
+ method: 'POST',
+ body: JSON.stringify({ request_id: opts.requestId, session_id: opts.sessionId, lang: opts.lang }),
+ })
+ }
+
+ createSSEStream(requestId: string): EventSource {
+ return new EventSource(`${this.baseUrl}/stream?request_id=${requestId}`)
+ }
+
+ async deleteMessage(opts: {
+ sessionId: string
+ userSeq: number
+ deleteUser?: boolean
+ cascade?: boolean
+ }): Promise<{ status: string; deleted: number }> {
+ return this.request('/api/messages/delete', {
+ method: 'POST',
+ body: JSON.stringify({
+ session_id: opts.sessionId,
+ user_seq: opts.userSeq,
+ delete_user: opts.deleteUser ?? true,
+ cascade: opts.cascade ?? false,
+ }),
+ })
+ }
+
+ // ---------------------------------------------------------
+ // Upload / files
+ // ---------------------------------------------------------
+
+ async uploadFile(file: File, sessionId?: string): Promise<{
+ status: string
+ file_path: string
+ file_name: string
+ file_type: string
+ preview_url: string
+ }> {
+ const formData = new FormData()
+ formData.append('file', file)
+ if (sessionId) formData.append('session_id', sessionId)
+ const res = await fetch(`${this.baseUrl}/upload`, {
+ method: 'POST',
+ body: formData,
+ credentials: 'include',
+ })
+ return res.json()
+ }
+
+ getFileUrl(previewUrl: string): string {
+ if (/^https?:\/\//.test(previewUrl)) return previewUrl
+ return `${this.baseUrl}${previewUrl}`
+ }
+
+ getServeFileUrl(absPath: string): string {
+ return `${this.baseUrl}/api/file?path=${encodeURIComponent(absPath)}`
+ }
+
+ // ---------------------------------------------------------
+ // Sessions
+ // ---------------------------------------------------------
+
+ async getSessions(page = 1, pageSize = 50): Promise {
+ return this.request<{ status: string } & SessionsPage>(`/api/sessions?page=${page}&page_size=${pageSize}`)
+ }
+
+ async deleteSession(sessionId: string): Promise {
+ return this.request(`/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' })
+ }
+
+ async renameSession(sessionId: string, title: string): Promise {
+ return this.request(`/api/sessions/${encodeURIComponent(sessionId)}`, {
+ method: 'PUT',
+ body: JSON.stringify({ title }),
+ })
+ }
+
+ async generateSessionTitle(sessionId: string, userMessage: string, assistantReply?: string): Promise<{ status: string; title: string }> {
+ return this.request(`/api/sessions/${encodeURIComponent(sessionId)}/generate_title`, {
+ method: 'POST',
+ body: JSON.stringify({ user_message: userMessage, assistant_reply: assistantReply }),
+ })
+ }
+
+ async clearContext(sessionId: string): Promise<{ status: string; context_start_seq: number }> {
+ return this.request(`/api/sessions/${encodeURIComponent(sessionId)}/clear_context`, { method: 'POST' })
+ }
+
+ async getHistory(sessionId: string, page = 1, pageSize = 20): Promise {
+ return this.request<{ status: string } & HistoryPage>(
+ `/api/history?session_id=${encodeURIComponent(sessionId)}&page=${page}&page_size=${pageSize}`
+ )
+ }
+
+ // ---------------------------------------------------------
+ // Config
+ // ---------------------------------------------------------
+
+ async getConfig(): Promise {
+ return this.request<{ status: string } & ConfigData>('/config')
+ }
+
+ async updateConfig(updates: Record): Promise<{ status: string; applied: Record }> {
+ return this.request('/config', {
+ method: 'POST',
+ body: JSON.stringify({ updates }),
+ })
+ }
+
+ // ---------------------------------------------------------
+ // Models console
+ // ---------------------------------------------------------
+
+ async getModels(): Promise {
+ return this.request<{ status: string } & ModelsData>('/api/models')
+ }
+
+ async modelsAction(action: ModelsAction): Promise & { status: string }> {
+ return this.request('/api/models', {
+ method: 'POST',
+ body: JSON.stringify(action),
+ })
+ }
+
+ // ---------------------------------------------------------
+ // Channels
+ // ---------------------------------------------------------
+
+ async getChannels(): Promise {
+ const data = await this.request<{ status: string; channels: ChannelInfo[] }>('/api/channels')
+ return data.channels
+ }
+
+ async channelAction(
+ action: ChannelAction,
+ channel: string,
+ config?: Record
+ ): Promise & { status: string }> {
+ return this.request('/api/channels', {
+ method: 'POST',
+ body: JSON.stringify({ action, channel, config }),
+ })
+ }
+
+ // Weixin QR login
+ async getWeixinQr(): Promise<{ status: string; qrcode_url?: string; qr_image?: string; source?: string; message?: string }> {
+ return this.request('/api/weixin/qrlogin')
+ }
+
+ async weixinQrAction(action: 'poll' | 'refresh'): Promise & { status: string }> {
+ return this.request('/api/weixin/qrlogin', {
+ method: 'POST',
+ body: JSON.stringify({ action }),
+ })
+ }
+
+ // Feishu one-click register
+ async getFeishuRegister(): Promise<{ status: string; qrcode_url?: string; qr_image?: string; expire_in?: number; message?: string }> {
+ return this.request('/api/feishu/register')
+ }
+
+ async feishuRegisterPoll(): Promise & { status: string }> {
+ return this.request('/api/feishu/register', {
+ method: 'POST',
+ body: JSON.stringify({ action: 'poll' }),
+ })
+ }
+
+ // ---------------------------------------------------------
+ // Tools & skills
+ // ---------------------------------------------------------
+
+ async getTools(): Promise {
+ const data = await this.request<{ status: string; tools: ToolInfo[] }>('/api/tools')
+ return data.tools
+ }
+
+ async getSkills(): Promise {
+ const data = await this.request<{ status: string; skills: SkillInfo[] }>('/api/skills')
+ return data.skills
+ }
+
+ async toggleSkill(name: string, action: 'open' | 'close'): Promise {
+ return this.request('/api/skills', {
+ method: 'POST',
+ body: JSON.stringify({ action, name }),
+ })
+ }
+
+ // ---------------------------------------------------------
+ // Memory
+ // ---------------------------------------------------------
+
+ async getMemoryList(page = 1, pageSize = 20, category: MemoryCategory = 'memory'): Promise {
+ return this.request<{ status: string } & MemoryPage>(
+ `/api/memory?page=${page}&page_size=${pageSize}&category=${category}`
+ )
+ }
+
+ async getMemoryContent(filename: string, category: MemoryCategory = 'memory'): Promise {
+ const data = await this.request<{ status: string; content: string }>(
+ `/api/memory/content?filename=${encodeURIComponent(filename)}&category=${category}`
+ )
+ return data.content
+ }
+
+ // ---------------------------------------------------------
+ // Knowledge
+ // ---------------------------------------------------------
+
+ async getKnowledgeList(): Promise {
+ return this.request<{ status: string } & KnowledgeList>('/api/knowledge/list')
+ }
+
+ async readKnowledge(path: string): Promise<{ status: string; content: string; path: string }> {
+ return this.request(`/api/knowledge/read?path=${encodeURIComponent(path)}`)
+ }
+
+ async getKnowledgeGraph(): Promise {
+ return this.request('/api/knowledge/graph')
+ }
+
+ async knowledgeAction(req: KnowledgeAction): Promise & { status: string }> {
+ return this.request('/api/knowledge/action', {
+ method: 'POST',
+ body: JSON.stringify(req),
+ })
+ }
+
+ // ---------------------------------------------------------
+ // Scheduler
+ // ---------------------------------------------------------
+
+ async getSchedulerTasks(): Promise {
+ const data = await this.request<{ status: string; tasks: SchedulerTask[] }>('/api/scheduler')
+ return data.tasks
+ }
+
+ async toggleTask(taskId: string, enabled: boolean): Promise<{ status: string; task: SchedulerTask }> {
+ return this.request('/api/scheduler/toggle', {
+ method: 'POST',
+ body: JSON.stringify({ task_id: taskId, enabled }),
+ })
+ }
+
+ async updateTask(taskId: string, updates: Partial>): Promise<{ status: string; task: SchedulerTask }> {
+ return this.request('/api/scheduler/update', {
+ method: 'POST',
+ body: JSON.stringify({ task_id: taskId, ...updates }),
+ })
+ }
+
+ async deleteTask(taskId: string): Promise {
+ return this.request('/api/scheduler/delete', {
+ method: 'POST',
+ body: JSON.stringify({ task_id: taskId }),
+ })
+ }
+
+ // ---------------------------------------------------------
+ // Voice
+ // ---------------------------------------------------------
+
+ async voiceAsr(audio: File | Blob): Promise<{ status: string; text?: string; audio_url?: string; message?: string }> {
+ const formData = new FormData()
+ formData.append('file', audio, 'recording.webm')
+ const res = await fetch(`${this.baseUrl}/api/voice/asr`, {
+ method: 'POST',
+ body: formData,
+ credentials: 'include',
+ })
+ return res.json()
+ }
+
+ async voiceTts(text: string, sessionId?: string): Promise<{ status: string; audio_url?: string; message?: string }> {
+ return this.request('/api/voice/tts', {
+ method: 'POST',
+ body: JSON.stringify({ text, session_id: sessionId }),
+ })
+ }
+
+ // ---------------------------------------------------------
+ // Logs / version
+ // ---------------------------------------------------------
+
+ createLogStream(): EventSource {
+ return new EventSource(`${this.baseUrl}/api/logs`)
+ }
+
+ async getVersion(): Promise {
+ const data = await this.request<{ version: string }>('/api/version')
+ return data.version
+ }
+
+ // ---------------------------------------------------------
+ // Auth (web_password) — placeholder for future use
+ // ---------------------------------------------------------
+
+ async authCheck(): Promise<{ status: string; auth_required: boolean; authenticated?: boolean }> {
+ return this.request('/auth/check')
+ }
+
+ async authLogin(password: string): Promise {
+ return this.request('/auth/login', {
+ method: 'POST',
+ body: JSON.stringify({ password }),
+ })
+ }
+
+ async authLogout(): Promise {
+ return this.request('/auth/logout', { method: 'POST' })
+ }
+}
+
+export const apiClient = new ApiClient()
+export default apiClient
diff --git a/desktop/src/renderer/src/components/ChatInput.tsx b/desktop/src/renderer/src/components/ChatInput.tsx
new file mode 100644
index 00000000..9138ca8a
--- /dev/null
+++ b/desktop/src/renderer/src/components/ChatInput.tsx
@@ -0,0 +1,326 @@
+import React, { useState, useRef, useCallback, useEffect, forwardRef, useImperativeHandle } from 'react'
+import { Plus, Paperclip, Send, Square, X, File as FileIcon, Loader2 } from 'lucide-react'
+import { t } from '../i18n'
+import type { Attachment } from '../types'
+import apiClient from '../api/client'
+
+export type ChatInputHandle = (text: string, attachments: Attachment[]) => void
+
+interface SlashCommand {
+ cmd: string
+ desc: string
+ action: 'new' | 'clear'
+}
+
+interface ChatInputProps {
+ onSend: (message: string, attachments: Attachment[]) => void
+ onNewChat: () => void
+ onStop: () => void
+ onClearContext: () => void
+ isStreaming: boolean
+ sessionId: string
+}
+
+const ChatInput = forwardRef(function ChatInput(
+ { onSend, onNewChat, onStop, onClearContext, isStreaming, sessionId },
+ ref
+) {
+ const [text, setText] = useState('')
+ const [attachments, setAttachments] = useState([])
+ const [uploading, setUploading] = useState(false)
+ const [dragOver, setDragOver] = useState(false)
+ const [slashOpen, setSlashOpen] = useState(false)
+ const [slashIndex, setSlashIndex] = useState(0)
+ const composingRef = useRef(false)
+ const textareaRef = useRef(null)
+ const fileInputRef = useRef(null)
+
+ const slashCommands: SlashCommand[] = [
+ { cmd: '/new', desc: t('session_new'), action: 'new' },
+ { cmd: '/clear', desc: t('chat_clear_context'), action: 'clear' },
+ ]
+ const filtered = slashCommands.filter((c) => c.cmd.startsWith(text.trim().toLowerCase()))
+
+ const resetHeight = () => {
+ if (textareaRef.current) textareaRef.current.style.height = '42px'
+ }
+
+ // Allow the parent to load a draft (e.g. when editing a past user message).
+ useImperativeHandle(ref, () => (draft: string, atts: Attachment[]) => {
+ setText(draft)
+ setAttachments(atts)
+ requestAnimationFrame(() => {
+ const el = textareaRef.current
+ if (el) {
+ el.focus()
+ el.style.height = '42px'
+ el.style.height = Math.min(el.scrollHeight, 180) + 'px'
+ }
+ })
+ })
+
+ const runSlash = (c: SlashCommand) => {
+ setText('')
+ setSlashOpen(false)
+ resetHeight()
+ if (c.action === 'new') onNewChat()
+ else if (c.action === 'clear') onClearContext()
+ }
+
+ const handleSubmit = useCallback(() => {
+ const trimmed = text.trim()
+ if (!trimmed && attachments.length === 0) return
+ if (isStreaming) return
+ onSend(trimmed, attachments)
+ setText('')
+ setAttachments([])
+ setSlashOpen(false)
+ resetHeight()
+ }, [text, attachments, isStreaming, onSend])
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ // Slash menu navigation
+ if (slashOpen && filtered.length > 0) {
+ if (e.key === 'ArrowDown') {
+ e.preventDefault()
+ setSlashIndex((i) => (i + 1) % filtered.length)
+ return
+ }
+ if (e.key === 'ArrowUp') {
+ e.preventDefault()
+ setSlashIndex((i) => (i - 1 + filtered.length) % filtered.length)
+ return
+ }
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault()
+ runSlash(filtered[slashIndex])
+ return
+ }
+ if (e.key === 'Escape') {
+ setSlashOpen(false)
+ return
+ }
+ }
+ // Don't submit while IME is composing (Chinese input)
+ if (e.key === 'Enter' && !e.shiftKey && !composingRef.current) {
+ e.preventDefault()
+ handleSubmit()
+ }
+ }
+
+ const handleTextChange = (e: React.ChangeEvent) => {
+ const v = e.target.value
+ setText(v)
+ const el = e.target
+ el.style.height = '42px'
+ el.style.height = Math.min(el.scrollHeight, 180) + 'px'
+ // open slash menu when the input starts with "/" and has no space
+ setSlashOpen(v.startsWith('/') && !v.includes(' '))
+ setSlashIndex(0)
+ }
+
+ const uploadFiles = async (files: File[]) => {
+ if (!files.length) return
+ setUploading(true)
+ try {
+ for (const file of files) {
+ const result = await apiClient.uploadFile(file, sessionId)
+ if (result.status === 'success') {
+ setAttachments((prev) => [
+ ...prev,
+ {
+ file_path: result.file_path,
+ file_name: result.file_name,
+ file_type: result.file_type as Attachment['file_type'],
+ preview_url: result.preview_url,
+ },
+ ])
+ }
+ }
+ } catch (err) {
+ console.error('Upload failed:', err)
+ } finally {
+ setUploading(false)
+ }
+ }
+
+ const handleFileSelect = async (e: React.ChangeEvent) => {
+ const files = e.target.files
+ if (files) await uploadFiles(Array.from(files))
+ if (fileInputRef.current) fileInputRef.current.value = ''
+ }
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault()
+ setDragOver(false)
+ const files = Array.from(e.dataTransfer.files || [])
+ if (files.length) uploadFiles(files)
+ }
+
+ const handlePaste = (e: React.ClipboardEvent) => {
+ const items = e.clipboardData?.items
+ if (!items) return
+ const files: File[] = []
+ for (const item of Array.from(items)) {
+ if (item.kind === 'file') {
+ const f = item.getAsFile()
+ if (f) files.push(f)
+ }
+ }
+ if (files.length) {
+ e.preventDefault()
+ uploadFiles(files)
+ }
+ }
+
+ const removeAttachment = (index: number) => {
+ setAttachments((prev) => prev.filter((_, i) => i !== index))
+ }
+
+ // keep slash index in range
+ useEffect(() => {
+ if (slashIndex >= filtered.length) setSlashIndex(0)
+ }, [filtered.length, slashIndex])
+
+ const canSend = !isStreaming && (!!text.trim() || attachments.length > 0)
+
+ return (
+
+
{
+ e.preventDefault()
+ setDragOver(true)
+ }}
+ onDragLeave={() => setDragOver(false)}
+ onDrop={handleDrop}
+ >
+ {dragOver && (
+
+ {t('input_placeholder')}
+
+ )}
+
+ {/* Slash command menu */}
+ {slashOpen && filtered.length > 0 && (
+
+ {filtered.map((c, i) => (
+ setSlashIndex(i)}
+ onClick={() => runSlash(c)}
+ className={`w-full flex items-center gap-3 px-3 py-2 text-left cursor-pointer transition-colors ${
+ i === slashIndex ? 'bg-accent-soft' : 'hover:bg-surface-2'
+ }`}
+ >
+ {c.cmd}
+ {c.desc}
+
+ ))}
+
+ )}
+
+ {/* Attachment preview */}
+ {attachments.length > 0 && (
+
+ {attachments.map((att, i) => (
+
+ {att.file_type === 'image' && att.preview_url ? (
+
+
+
removeAttachment(i)}
+ className="absolute -top-1 -right-1 w-[18px] h-[18px] rounded-full bg-danger text-white flex items-center justify-center cursor-pointer"
+ >
+
+
+
+ ) : (
+
+
+ {att.file_name}
+ removeAttachment(i)}
+ className="absolute -top-1 -right-1 w-[18px] h-[18px] rounded-full bg-danger text-white flex items-center justify-center cursor-pointer"
+ >
+
+
+
+ )}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
fileInputRef.current?.click()}
+ disabled={uploading}
+ className="w-9 h-9 flex items-center justify-center rounded-btn text-content-secondary hover:text-accent hover:bg-accent-soft cursor-pointer transition-colors disabled:opacity-50"
+ title={t('chat_attach')}
+ >
+ {uploading ? : }
+
+
+
+
+
+
+
+ )
+})
+
+export default ChatInput
diff --git a/desktop/src/renderer/src/components/KnowledgeGraph.tsx b/desktop/src/renderer/src/components/KnowledgeGraph.tsx
new file mode 100644
index 00000000..cd508c52
--- /dev/null
+++ b/desktop/src/renderer/src/components/KnowledgeGraph.tsx
@@ -0,0 +1,407 @@
+import React, { useEffect, useMemo, useRef, useState } from 'react'
+import type { KnowledgeGraph as KnowledgeGraphData } from '../types'
+
+interface SimNode {
+ id: string
+ label: string
+ category: string
+ x: number
+ y: number
+ vx: number
+ vy: number
+ fx: number | null
+ fy: number | null
+ degree: number
+}
+
+interface KnowledgeGraphProps {
+ data: KnowledgeGraphData
+ onSelect: (id: string, label: string) => void
+}
+
+// d3.schemeTableau10 — keep the web client's palette for visual parity.
+const TABLEAU10 = [
+ '#4e79a7',
+ '#f28e2c',
+ '#e15759',
+ '#76b7b2',
+ '#59a14f',
+ '#edc949',
+ '#af7aa1',
+ '#ff9da7',
+ '#9c755f',
+ '#bab0ab',
+]
+
+const nodeRadius = (degree: number) => Math.max(4, Math.min(12, 4 + degree * 1.4))
+
+// A dependency-free force-directed graph with wheel zoom, canvas pan and node
+// drag. The physics loop writes positions DIRECTLY to the DOM (like d3) instead
+// of calling setState per frame, so React never re-renders during the
+// simulation — this is what keeps it from flickering.
+const KnowledgeGraph: React.FC = ({ data, onSelect }) => {
+ const wrapRef = useRef(null)
+ const svgRef = useRef(null)
+ const sizeRef = useRef({ w: 800, h: 560 })
+ const [hover, setHover] = useState(null)
+ // Bumping this re-arms the physics loop (used while dragging) without
+ // rebuilding the model, preserving current node positions.
+ const [warmTick, setWarmTick] = useState(0)
+
+ // View transform (pan + zoom).
+ const viewRef = useRef({ k: 1, x: 0, y: 0 })
+
+ // Build the immutable model once per data change.
+ const model = useMemo(() => {
+ const degree = new Map()
+ data.links.forEach((l) => {
+ degree.set(l.source, (degree.get(l.source) || 0) + 1)
+ degree.set(l.target, (degree.get(l.target) || 0) + 1)
+ })
+ const categories = Array.from(new Set(data.nodes.map((n) => n.category || 'default')))
+ const colorOf = (cat: string) => TABLEAU10[categories.indexOf(cat) % TABLEAU10.length]
+
+ const n = data.nodes.length || 1
+ const { w, h } = sizeRef.current
+ const cx = w / 2
+ const cy = h / 2
+ const nodes: SimNode[] = data.nodes.map((nd, i) => {
+ const angle = (i / n) * Math.PI * 2
+ const radius = Math.min(w, h) * 0.32
+ return {
+ id: nd.id,
+ label: nd.label,
+ category: nd.category || 'default',
+ x: cx + Math.cos(angle) * radius,
+ y: cy + Math.sin(angle) * radius,
+ vx: 0,
+ vy: 0,
+ fx: null,
+ fy: null,
+ degree: degree.get(nd.id) || 0,
+ }
+ })
+ const valid = new Set(nodes.map((x) => x.id))
+ const byId = new Map(nodes.map((x) => [x.id, x]))
+ const links = data.links
+ .filter((l) => valid.has(l.source) && valid.has(l.target))
+ .map((l, i) => ({ key: i, a: byId.get(l.source)!, b: byId.get(l.target)! }))
+ const adjacency = new Map>()
+ data.links.forEach((l) => {
+ if (!valid.has(l.source) || !valid.has(l.target)) return
+ if (!adjacency.has(l.source)) adjacency.set(l.source, new Set())
+ if (!adjacency.has(l.target)) adjacency.set(l.target, new Set())
+ adjacency.get(l.source)!.add(l.target)
+ adjacency.get(l.target)!.add(l.source)
+ })
+ return { nodes, links, adjacency, categories, colorOf, byId }
+ }, [data])
+
+ // DOM refs for imperative position updates.
+ const rootRef = useRef(null)
+ const lineEls = useRef(new Map())
+ const groupEls = useRef(new Map())
+
+ // Track container size in a ref; never triggers a re-render on its own.
+ useEffect(() => {
+ const el = wrapRef.current
+ if (!el) return
+ const apply = () => {
+ sizeRef.current = { w: el.clientWidth || 800, h: el.clientHeight || 560 }
+ }
+ apply()
+ const ro = new ResizeObserver(apply)
+ ro.observe(el)
+ return () => ro.disconnect()
+ }, [])
+
+ // Physics loop. Restarts only when the model (data) changes. Writes to DOM.
+ // Uses d3-style alpha cooling so it always settles and stops the rAF.
+ useEffect(() => {
+ const { nodes, links } = model
+ if (nodes.length === 0) return
+ let raf = 0
+ let alive = true
+ // Global cooling factor; decays toward 0 and scales how far nodes move.
+ let alpha = 1
+ const alphaDecay = 0.018
+ const alphaMin = 0.005
+
+ const paint = () => {
+ links.forEach(({ key, a, b }) => {
+ const el = lineEls.current.get(key)
+ if (!el) return
+ el.setAttribute('x1', String(a.x))
+ el.setAttribute('y1', String(a.y))
+ el.setAttribute('x2', String(b.x))
+ el.setAttribute('y2', String(b.y))
+ })
+ nodes.forEach((node) => {
+ const el = groupEls.current.get(node.id)
+ if (el) el.setAttribute('transform', `translate(${node.x},${node.y})`)
+ })
+ }
+
+ const step = () => {
+ if (!alive) return
+ const { w, h } = sizeRef.current
+ const cx = w / 2
+ const cy = h / 2
+ const repulsion = 9000
+ const springLen = 80
+ const spring = 0.04
+ const centering = 0.012
+ const dragging = nodes.some((node) => node.fx != null)
+
+ // Reset accumulated velocity each tick (alpha-scaled displacement) so the
+ // system can't accumulate energy and oscillate.
+ nodes.forEach((node) => {
+ node.vx = 0
+ node.vy = 0
+ })
+
+ // Repulsion + collision: nodes push apart, never overlap their radii.
+ for (let i = 0; i < nodes.length; i++) {
+ const a = nodes[i]
+ const ra = nodeRadius(a.degree)
+ for (let j = i + 1; j < nodes.length; j++) {
+ const b = nodes[j]
+ let dx = a.x - b.x
+ let dy = a.y - b.y
+ let d2 = dx * dx + dy * dy
+ if (d2 < 0.01) {
+ dx = Math.random() - 0.5
+ dy = Math.random() - 0.5
+ d2 = 0.01
+ }
+ let d = Math.sqrt(d2)
+ let f = repulsion / d2
+ // Hard collision: strongly separate if closer than combined radii.
+ const minDist = ra + nodeRadius(b.degree) + 14
+ if (d < minDist) f += (minDist - d) * 0.6
+ a.vx += (dx / d) * f
+ a.vy += (dy / d) * f
+ b.vx -= (dx / d) * f
+ b.vy -= (dy / d) * f
+ }
+ }
+ links.forEach(({ a, b }) => {
+ const dx = b.x - a.x
+ const dy = b.y - a.y
+ const d = Math.sqrt(dx * dx + dy * dy) || 1
+ const f = (d - springLen) * spring
+ a.vx += (dx / d) * f
+ a.vy += (dy / d) * f
+ b.vx -= (dx / d) * f
+ b.vy -= (dy / d) * f
+ })
+ // Weak centering so the whole graph stays in view without collapsing.
+ nodes.forEach((node) => {
+ node.vx += (cx - node.x) * centering
+ node.vy += (cy - node.y) * centering
+ })
+
+ // Apply alpha-scaled displacement; pinned nodes stay put. Cap per-tick
+ // movement so strong initial forces don't fling nodes off-screen.
+ const maxStep = 30
+ nodes.forEach((node) => {
+ if (node.fx != null) {
+ node.x = node.fx
+ node.y = node.fy as number
+ return
+ }
+ let dx = node.vx * alpha
+ let dy = node.vy * alpha
+ const m = Math.hypot(dx, dy)
+ if (m > maxStep) {
+ dx = (dx / m) * maxStep
+ dy = (dy / m) * maxStep
+ }
+ node.x += dx
+ node.y += dy
+ })
+
+ paint()
+ alpha += (0 - alpha) * alphaDecay
+ // Keep running while cooling, or while a node is being dragged.
+ if (alpha > alphaMin || dragging) {
+ raf = requestAnimationFrame(step)
+ }
+ }
+ raf = requestAnimationFrame(step)
+ return () => {
+ alive = false
+ cancelAnimationFrame(raf)
+ }
+ // warmTick re-arms the loop on demand (e.g. while dragging) without
+ // rebuilding the model, so positions are preserved.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [model, warmTick])
+
+ // Apply the view transform imperatively (no re-render needed).
+ const applyView = () => {
+ const v = viewRef.current
+ if (rootRef.current) rootRef.current.setAttribute('transform', `translate(${v.x},${v.y}) scale(${v.k})`)
+ }
+ useEffect(applyView)
+
+ // Convert a pointer event to graph (pre-transform) coordinates.
+ const toGraph = (clientX: number, clientY: number) => {
+ const rect = svgRef.current!.getBoundingClientRect()
+ const v = viewRef.current
+ return { x: (clientX - rect.left - v.x) / v.k, y: (clientY - rect.top - v.y) / v.k }
+ }
+
+ // Wheel zoom centered on the cursor (matches d3.zoom scaleExtent [0.2, 5]).
+ const onWheel = (e: React.WheelEvent) => {
+ e.preventDefault()
+ const v = viewRef.current
+ const rect = svgRef.current!.getBoundingClientRect()
+ const px = e.clientX - rect.left
+ const py = e.clientY - rect.top
+ const factor = Math.exp(-e.deltaY * 0.0015)
+ const k = Math.min(5, Math.max(0.2, v.k * factor))
+ viewRef.current = { k, x: px - ((px - v.x) / v.k) * k, y: py - ((py - v.y) / v.k) * k }
+ applyView()
+ }
+
+ // Drag: on a node moves the node, on background pans the canvas.
+ const dragRef = useRef<
+ | { mode: 'node'; node: SimNode; moved: boolean }
+ | { mode: 'pan'; startX: number; startY: number; ox: number; oy: number }
+ | null
+ >(null)
+ const kick = () => setWarmTick((v) => v + 1)
+
+ const onPointerDownNode = (e: React.PointerEvent, node: SimNode) => {
+ e.stopPropagation()
+ ;(e.currentTarget as Element).setPointerCapture(e.pointerId)
+ const p = toGraph(e.clientX, e.clientY)
+ node.fx = p.x
+ node.fy = p.y
+ dragRef.current = { mode: 'node', node, moved: false }
+ kick()
+ }
+
+ const onPointerDownBg = (e: React.PointerEvent) => {
+ ;(e.currentTarget as Element).setPointerCapture(e.pointerId)
+ const v = viewRef.current
+ dragRef.current = { mode: 'pan', startX: e.clientX, startY: e.clientY, ox: v.x, oy: v.y }
+ }
+
+ const onPointerMove = (e: React.PointerEvent) => {
+ const drag = dragRef.current
+ if (!drag) return
+ if (drag.mode === 'node') {
+ const p = toGraph(e.clientX, e.clientY)
+ drag.node.fx = p.x
+ drag.node.fy = p.y
+ drag.moved = true
+ // Keep the loop warm for live dragging.
+ const el = groupEls.current.get(drag.node.id)
+ if (el) el.setAttribute('transform', `translate(${p.x},${p.y})`)
+ } else {
+ viewRef.current = { k: viewRef.current.k, x: drag.ox + (e.clientX - drag.startX), y: drag.oy + (e.clientY - drag.startY) }
+ applyView()
+ }
+ }
+
+ const onPointerUp = (e: React.PointerEvent, node?: SimNode) => {
+ const drag = dragRef.current
+ if (drag?.mode === 'node') {
+ drag.node.fx = null
+ drag.node.fy = null
+ if (node && !drag.moved) onSelect(node.id, node.label)
+ kick()
+ }
+ dragRef.current = null
+ try {
+ ;(e.target as Element).releasePointerCapture(e.pointerId)
+ } catch {
+ /* noop */
+ }
+ }
+
+ const { nodes, links, adjacency, categories, colorOf } = model
+ const { w, h } = sizeRef.current
+ const isDimmed = (id: string) => hover != null && hover !== id && !adjacency.get(hover)?.has(id)
+ const isLinkActive = (aId: string, bId: string) => hover === aId || hover === bId
+
+ return (
+
+
onPointerUp(e)}
+ >
+
+ {links.map(({ key, a, b }) => {
+ const active = isLinkActive(a.id, b.id)
+ return (
+ {
+ if (el) lineEls.current.set(key, el)
+ else lineEls.current.delete(key)
+ }}
+ x1={a.x}
+ y1={a.y}
+ x2={b.x}
+ y2={b.y}
+ stroke="#94a3b8"
+ strokeOpacity={hover ? (active ? 0.8 : 0.1) : 0.3}
+ strokeWidth={1}
+ />
+ )
+ })}
+ {nodes.map((n) => {
+ const r = nodeRadius(n.degree)
+ const dim = isDimmed(n.id)
+ return (
+ {
+ if (el) groupEls.current.set(n.id, el)
+ else groupEls.current.delete(n.id)
+ }}
+ transform={`translate(${n.x},${n.y})`}
+ className="cursor-pointer"
+ opacity={dim ? 0.2 : 1}
+ onMouseEnter={() => setHover(n.id)}
+ onMouseLeave={() => setHover(null)}
+ onPointerDown={(e) => onPointerDownNode(e, n)}
+ onPointerUp={(e) => onPointerUp(e, n)}
+ >
+
+ {(hover === n.id || n.degree >= 3) && (
+
+ {n.label.length > 15 ? n.label.slice(0, 14) + '…' : n.label}
+
+ )}
+
+ )
+ })}
+
+
+
+ {/* Category legend, mirrors the web client. */}
+ {categories.length > 0 && (
+
+ {categories.map((cat) => (
+
+
+ {cat}
+
+ ))}
+
+ )}
+
+ )
+}
+
+export default KnowledgeGraph
diff --git a/desktop/src/renderer/src/components/Markdown.tsx b/desktop/src/renderer/src/components/Markdown.tsx
new file mode 100644
index 00000000..a27e4afc
--- /dev/null
+++ b/desktop/src/renderer/src/components/Markdown.tsx
@@ -0,0 +1,109 @@
+import React, { useMemo, useRef, useCallback } from 'react'
+import MarkdownIt from 'markdown-it'
+import hljs from 'highlight.js'
+import { t } from '../i18n'
+
+/**
+ * Markdown renderer aligned 1:1 with the web console (markdown-it + highlight.js
+ * + GitHub themes). Using the same engine guarantees identical line-break,
+ * linkify and code-highlight behavior across web and desktop.
+ */
+
+const md: MarkdownIt = new MarkdownIt({
+ html: false,
+ breaks: true,
+ linkify: true,
+ typographer: true,
+ highlight(str, lang) {
+ if (lang && hljs.getLanguage(lang)) {
+ try {
+ return hljs.highlight(str, { language: lang }).value
+ } catch {
+ /* fall through */
+ }
+ }
+ try {
+ return hljs.highlightAuto(str).value
+ } catch {
+ return ''
+ }
+ },
+})
+
+// Open links in a new tab safely.
+const defaultLinkOpen =
+ md.renderer.rules.link_open ||
+ function (tokens, idx, options, _env, self) {
+ return self.renderToken(tokens, idx, options)
+ }
+md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
+ tokens[idx].attrPush(['target', '_blank'])
+ tokens[idx].attrPush(['rel', 'noopener noreferrer'])
+ return defaultLinkOpen(tokens, idx, options, env, self)
+}
+
+// Wrap fenced code blocks so we can render a header (lang + copy button).
+const defaultFence =
+ md.renderer.rules.fence ||
+ function (tokens, idx, options, _env, self) {
+ return self.renderToken(tokens, idx, options)
+ }
+md.renderer.rules.fence = function (tokens, idx, options, env, self) {
+ const token = tokens[idx]
+ const info = token.info ? token.info.trim().split(/\s+/)[0] : ''
+ // Ensure the `hljs` class is present so the GitHub theme background/base
+ // color applies (markdown-it only adds language-* by default).
+ let rendered = defaultFence(tokens, idx, options, env, self)
+ if (rendered.includes('')
+ }
+ return (
+ `` +
+ `` +
+ rendered +
+ `
`
+ )
+}
+
+interface MarkdownProps {
+ content: string
+}
+
+const Markdown: React.FC = ({ content }) => {
+ const rootRef = useRef(null)
+
+ const html = useMemo(() => md.render(content || ''), [content])
+
+ // Delegate copy clicks on code blocks (buttons are injected as raw HTML).
+ const handleClick = useCallback((e: React.MouseEvent) => {
+ const target = e.target as HTMLElement
+ const btn = target.closest('.code-copy-btn') as HTMLElement | null
+ if (!btn) return
+ const pre = btn.closest('.code-block-wrapper')?.querySelector('pre')
+ if (!pre) return
+ navigator.clipboard.writeText(pre.textContent || '')
+ const original = btn.textContent
+ btn.textContent = t('msg_copied')
+ btn.classList.add('copied')
+ setTimeout(() => {
+ btn.textContent = original
+ btn.classList.remove('copied')
+ }, 1600)
+ }, [])
+
+ return (
+
+ )
+}
+
+export default Markdown
diff --git a/desktop/src/renderer/src/components/MessageBubble.tsx b/desktop/src/renderer/src/components/MessageBubble.tsx
new file mode 100644
index 00000000..ac6629a5
--- /dev/null
+++ b/desktop/src/renderer/src/components/MessageBubble.tsx
@@ -0,0 +1,156 @@
+import React, { useState } from 'react'
+import { Copy, Check, RefreshCw, Pencil, Trash2, File as FileIcon, Sprout } from 'lucide-react'
+import type { ChatMessage } from '../types'
+import { t } from '../i18n'
+import apiClient from '../api/client'
+import Markdown from './Markdown'
+import MessageSteps, { ThinkingStep } from './MessageSteps'
+
+interface MessageBubbleProps {
+ message: ChatMessage
+ onRegenerate?: (id: string) => void
+ onEdit?: (id: string) => void
+ onDelete?: (msg: ChatMessage) => void
+}
+
+function fmtTime(ts: number): string {
+ if (!ts) return ''
+ const d = new Date(ts * 1000)
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+}
+
+const HoverAction: React.FC<{ onClick: () => void; title: string; danger?: boolean; children: React.ReactNode }> = ({
+ onClick,
+ title,
+ danger,
+ children,
+}) => (
+
+ {children}
+
+)
+
+const MessageBubble: React.FC = ({ message, onRegenerate, onEdit, onDelete }) => {
+ const isUser = message.role === 'user'
+ const [copied, setCopied] = useState(false)
+
+ const copy = () => {
+ navigator.clipboard.writeText(message.content)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 1800)
+ }
+
+ if (isUser) {
+ return (
+
+ {message.attachments && message.attachments.length > 0 && (
+
+ {message.attachments.map((att, i) =>
+ att.file_type === 'image' && att.preview_url ? (
+
+ ) : (
+
+
+ {att.file_name}
+
+ )
+ )}
+
+ )}
+
+
+
{fmtTime(message.timestamp)}
+ {onEdit && message.userSeq != null && (
+
onEdit(message.id)} title={t('msg_edit')}>
+
+
+ )}
+ {onDelete && message.userSeq != null && (
+
onDelete(message)} title={t('msg_delete')} danger>
+
+
+ )}
+
+
+ )
+ }
+
+ // Assistant
+ const showCursor = message.isStreaming && !message.content && (!message.steps || message.steps.length === 0)
+
+ const hasSteps = !!(message.steps && message.steps.length > 0)
+ const hasLiveReasoning = !!(message.reasoning && message.isStreaming)
+
+ return (
+
+
+
+
+ {message.kind === 'evolution' && (
+
+
+ {t('msg_self_learned')}
+
+ )}
+
+ {/* Steps area (thinking / tools / intermediate content), web-aligned:
+ muted, separated from the final answer by a dashed divider. */}
+ {(hasSteps || hasLiveReasoning) && (
+
+ {hasLiveReasoning && }
+ {hasSteps && }
+
+ )}
+
+ {/* Final answer */}
+ {message.content &&
}
+
+ {showCursor && (
+
+
+
+
+
+ )}
+
+ {message.isStreaming && message.content && (
+
+ )}
+
+ {message.isCancelled &&
{t('msg_cancelled')}
}
+ {message.error &&
{message.error}
}
+
+
+ {/* Hover actions (only when finished) */}
+ {!message.isStreaming && (message.content || message.error) && (
+
+ {fmtTime(message.timestamp)}
+
+ {copied ? : }
+
+ {onRegenerate && (
+ onRegenerate(message.id)} title={t('msg_regenerate')}>
+
+
+ )}
+
+ )}
+
+
+ )
+}
+
+export default MessageBubble
diff --git a/desktop/src/renderer/src/components/MessageSteps.tsx b/desktop/src/renderer/src/components/MessageSteps.tsx
new file mode 100644
index 00000000..6bdbef80
--- /dev/null
+++ b/desktop/src/renderer/src/components/MessageSteps.tsx
@@ -0,0 +1,110 @@
+import React, { useState } from 'react'
+import { ChevronRight, Loader2, Check, X, Brain, Wrench } from 'lucide-react'
+import type { MessageStep } from '../types'
+import Markdown from './Markdown'
+
+/**
+ * Assistant reasoning / tool steps, styled to match the web console: small,
+ * muted, collapsible rows with an indented detail panel.
+ */
+
+const ThinkingStep: React.FC<{ content: string; streaming?: boolean }> = ({ content, streaming }) => {
+ const [expanded, setExpanded] = useState(false)
+ return (
+
+
setExpanded((v) => !v)}
+ >
+
+ {streaming ? 'Thinking…' : 'Thought for a moment'}
+
+
+ {expanded && (
+
+ {content}
+
+ )}
+
+ )
+}
+
+const ToolStep: React.FC<{ step: MessageStep }> = ({ step }) => {
+ const [expanded, setExpanded] = useState(false)
+ const running = step.status === 'running'
+ const isError = step.is_error || (!!step.status && step.status !== 'success' && !running)
+
+ const icon = running ? (
+
+ ) : isError ? (
+
+ ) : (
+
+ )
+
+ return (
+
+
setExpanded((v) => !v)}
+ >
+ {icon}
+
+ {step.name}
+ {step.execution_time !== undefined && (
+ {step.execution_time}s
+ )}
+
+
+ {expanded && (
+
+ {step.arguments && Object.keys(step.arguments).length > 0 && (
+
+
Input
+
+ {JSON.stringify(step.arguments, null, 2)}
+
+
+ )}
+ {step.result && (
+
+
+ {isError ? 'Error' : 'Output'}
+
+
+ {step.result.length > 4000 ? step.result.slice(0, 4000) + '\n… (truncated)' : step.result}
+
+
+ )}
+
+ )}
+
+ )
+}
+
+/** Renders an ordered list of assistant steps (thinking / content / tool). */
+const MessageSteps: React.FC<{ steps: MessageStep[] }> = ({ steps }) => {
+ if (!steps.length) return null
+ return (
+
+ {steps.map((step, i) => {
+ if (step.type === 'thinking') return
+ if (step.type === 'tool') return
+ if (step.type === 'content' && step.content)
+ return (
+
+
+
+ )
+ return null
+ })}
+
+ )
+}
+
+export { ThinkingStep, ToolStep }
+export default MessageSteps
diff --git a/desktop/src/renderer/src/components/OnboardingWizard.tsx b/desktop/src/renderer/src/components/OnboardingWizard.tsx
new file mode 100644
index 00000000..cde8baa2
--- /dev/null
+++ b/desktop/src/renderer/src/components/OnboardingWizard.tsx
@@ -0,0 +1,293 @@
+import React, { useEffect, useMemo, useState } from 'react'
+import { Sparkles, KeyRound, Loader2, ArrowRight, ArrowLeft, ExternalLink } from 'lucide-react'
+import { t, getLang, setLang, type Lang } from '../i18n'
+import apiClient from '../api/client'
+import type { ModelsData } from '../types'
+import { Field, Dropdown, TextInput, type DropdownOption } from '../pages/settings/primitives'
+import { resolveModels, providerLabel } from '../pages/settings/modelsHelpers'
+import { useOnboardingStore } from '../store/onboardingStore'
+
+interface OnboardingWizardProps {
+ // Called after the wizard finishes so the host can refresh language/state.
+ onDone: () => void
+}
+
+const TOTAL_STEPS = 2
+
+// Optional "where to get an API key" console link, per provider.
+const PROVIDER_KEY_CONSOLE: Record = {
+ linkai: 'https://link-ai.tech/console/interface',
+}
+
+// First-run guided setup: language -> chat model (provider + key + model).
+// After saving the model the user goes straight into the chat (no extra
+// confirmation step). Rendered as a full-screen overlay above the main UI;
+// reuses the same models API and primitives as the settings page.
+const OnboardingWizard: React.FC = ({ onDone }) => {
+ const finish = useOnboardingStore((s) => s.finish)
+
+ const [step, setStep] = useState(1)
+ const [lang, setLangState] = useState(getLang())
+ const [models, setModels] = useState(null)
+
+ // Step 2 form state.
+ const [provider, setProvider] = useState('')
+ const [apiKey, setApiKey] = useState('')
+ const [apiBase, setApiBase] = useState('')
+ const [model, setModel] = useState('')
+
+ const [saving, setSaving] = useState(false)
+ const [error, setError] = useState('')
+
+ // Load the models console data once for the provider/model dropdowns.
+ useEffect(() => {
+ apiClient
+ .getModels()
+ .then(setModels)
+ .catch(() => setError(t('onboarding_save_failed')))
+ }, [])
+
+ // Persist the auto-detected default language on first show so the pre-selected
+ // option (driven by OS locale) also reaches the backend, even if the user
+ // doesn't tap the language buttons.
+ useEffect(() => {
+ if (!localStorage.getItem('cow_lang')) switchLang(lang)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const providerOptions: DropdownOption[] = useMemo(() => {
+ const chat = models?.capabilities?.chat
+ const ids = chat?.providers || []
+ return ids.map((id) => ({ value: id, label: providerLabel(models, id) }))
+ }, [models])
+
+ const modelOptions: DropdownOption[] = useMemo(() => {
+ return resolveModels(models, provider, models?.capabilities?.chat?.provider_models).map((o) => ({
+ value: o.value,
+ label: o.value,
+ hint: o.hint,
+ }))
+ }, [models, provider])
+
+ // The currently selected provider's api_base placeholder/default, if any.
+ const providerMeta = models?.providers?.find((p) => p.id === provider)
+ const apiBasePlaceholder = providerMeta?.api_base_placeholder || providerMeta?.api_base_default
+
+ const handleProvider = (id: string) => {
+ setProvider(id)
+ setApiBase('')
+ const first = resolveModels(models, id, models?.capabilities?.chat?.provider_models)[0]
+ setModel(first?.value || '')
+ }
+
+ const switchLang = (next: Lang) => {
+ setLang(next)
+ setLangState(next)
+ // Mirror the choice to the backend so the agent/logs use the same language
+ // (matches BasicSettings). Non-blocking: the UI already switched locally.
+ apiClient.updateConfig({ cow_lang: next }).catch(() => {})
+ }
+
+ // Step 1 (language) can always advance; step 2 needs a provider, key, model.
+ const canNext = step === 1 || (!!provider && !!apiKey.trim() && !!model)
+
+ const goNext = async () => {
+ setError('')
+ // Step 1 (language) just advances to the model step.
+ if (step === 1) {
+ setStep(2)
+ return
+ }
+ // Step 2 is the last step: persist the provider credentials, point the chat
+ // capability at it, then finish straight into the chat (no extra step).
+ setSaving(true)
+ try {
+ await apiClient.modelsAction({
+ action: 'set_provider',
+ provider_id: provider,
+ api_key: apiKey.trim(),
+ ...(apiBase.trim() ? { api_base: apiBase.trim() } : {}),
+ })
+ await apiClient.modelsAction({
+ action: 'set_capability',
+ capability: 'chat',
+ provider_id: provider,
+ model,
+ })
+ } catch {
+ setSaving(false)
+ setError(t('onboarding_save_failed'))
+ return
+ }
+ setSaving(false)
+ complete()
+ }
+
+ const goBack = () => {
+ setError('')
+ setStep((s) => Math.max(1, s - 1))
+ }
+
+ const complete = () => {
+ finish()
+ onDone()
+ }
+
+ const stepLabel = t('onboarding_step').replace('{n}', String(step)).replace('{total}', String(TOTAL_STEPS))
+
+ return (
+
+
+ {/* Progress dots */}
+
+ {Array.from({ length: TOTAL_STEPS }).map((_, i) => (
+
+ ))}
+
+
+ {step === 1 && (
+
+
+
+
+
+
{t('onboarding_welcome_title')}
+
{t('onboarding_welcome_desc')}
+
+
+
+
+ {(['zh', 'en'] as Lang[]).map((l) => (
+ switchLang(l)}
+ className={`px-4 py-2.5 rounded-btn border text-sm font-medium cursor-pointer transition-colors ${
+ lang === l
+ ? 'border-accent bg-accent-soft text-accent'
+ : 'border-strong text-content-secondary hover:bg-surface-2'
+ }`}
+ >
+ {l === 'zh' ? '简体中文' : 'English'}
+
+ ))}
+
+
+
+
+ )}
+
+ {step === 2 && (
+
+
+
+
+
+
{t('onboarding_model_title')}
+
{t('onboarding_model_desc')}
+
+
+
+
+
+ {provider && (
+ <>
+
+ setApiKey(e.target.value)}
+ placeholder={t('onboarding_apikey_placeholder')}
+ className="font-mono"
+ />
+ {PROVIDER_KEY_CONSOLE[provider] && (
+
+ {t('onboarding_key_guide')}
+
+
+ )}
+
+ {providerMeta?.api_base_field && (
+
+ setApiBase(e.target.value)}
+ placeholder={apiBasePlaceholder || ''}
+ className="font-mono"
+ />
+
+ )}
+
+
+
+ >
+ )}
+ {error &&
{error}
}
+
+
+ )}
+
+ {/* Footer controls */}
+
+
{stepLabel}
+
+ {/* Step 2: back to language. */}
+ {step === 2 && (
+
+
+ {t('onboarding_back')}
+
+ )}
+ {/* Skip is available on every step: dismiss and go straight to chat. */}
+
+ {t('onboarding_skip')}
+
+ {/* Primary action: advance on step 1, save + finish on the last step. */}
+
+ {saving && }
+ {saving
+ ? t('onboarding_saving')
+ : step === TOTAL_STEPS
+ ? t('onboarding_finish')
+ : t('onboarding_next')}
+ {!saving && }
+
+
+
+
+
+ )
+}
+
+export default OnboardingWizard
diff --git a/desktop/src/renderer/src/components/QrLoginModal.tsx b/desktop/src/renderer/src/components/QrLoginModal.tsx
new file mode 100644
index 00000000..71ca484b
--- /dev/null
+++ b/desktop/src/renderer/src/components/QrLoginModal.tsx
@@ -0,0 +1,222 @@
+import React, { useEffect, useRef, useState } from 'react'
+import { Loader2, Check, RotateCcw } from 'lucide-react'
+import { t } from '../i18n'
+import apiClient from '../api/client'
+import { Modal } from '../pages/settings/primitives'
+
+type Provider = 'weixin' | 'feishu'
+type Phase = 'loading' | 'waiting' | 'scanned' | 'success' | 'error'
+
+interface QrLoginModalProps {
+ provider: Provider
+ onClose: () => void
+ // Fired once the channel is connected so the page can refresh.
+ onConnected: () => void
+}
+
+const POLL_INTERVAL = 2000
+
+// Shared QR-login / QR-register modal for WeChat and Feishu. Mirrors the web
+// console flow: fetch a QR, poll status, then connect the channel on success.
+const QrLoginModal: React.FC = ({ provider, onClose, onConnected }) => {
+ const [phase, setPhase] = useState('loading')
+ const [qr, setQr] = useState('')
+ const [openLink, setOpenLink] = useState('')
+ const [errMsg, setErrMsg] = useState('')
+ const timerRef = useRef | null>(null)
+ const aliveRef = useRef(true)
+
+ const stopPoll = () => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current)
+ timerRef.current = null
+ }
+ }
+
+ const fail = (msg: string) => {
+ if (!aliveRef.current) return
+ setPhase('error')
+ setErrMsg(msg)
+ }
+
+ // ---- WeChat: GET qr, POST poll {scaned|confirmed|expired} -----------------
+ const pollWeixin = () => {
+ timerRef.current = setTimeout(async () => {
+ if (!aliveRef.current) return
+ try {
+ const data = await apiClient.weixinQrAction('poll')
+ if (!aliveRef.current) return
+ if (data.status !== 'success') return pollWeixin()
+ const s = data.qr_status as string
+ if (s === 'confirmed') {
+ setPhase('success')
+ await apiClient.channelAction('connect', 'weixin', {})
+ if (aliveRef.current) onConnected()
+ } else if (s === 'expired' && (data.qr_image || data.qrcode_url)) {
+ setQr((data.qr_image as string) || (data.qrcode_url as string))
+ setPhase('waiting')
+ pollWeixin()
+ } else if (s === 'scaned') {
+ setPhase('scanned')
+ pollWeixin()
+ } else {
+ pollWeixin()
+ }
+ } catch {
+ pollWeixin()
+ }
+ }, POLL_INTERVAL)
+ }
+
+ const startWeixin = async () => {
+ setPhase('loading')
+ try {
+ const data = await apiClient.getWeixinQr()
+ if (!aliveRef.current) return
+ if (data.status !== 'success') return fail(data.message || t('weixin_scan_fail'))
+ setQr(data.qr_image || data.qrcode_url || '')
+ setPhase('waiting')
+ pollWeixin()
+ } catch {
+ fail(t('weixin_scan_fail'))
+ }
+ }
+
+ // ---- Feishu: GET qr, POST poll {done|expired|denied|error} ----------------
+ const pollFeishu = () => {
+ timerRef.current = setTimeout(async () => {
+ if (!aliveRef.current) return
+ try {
+ const data = await apiClient.feishuRegisterPoll()
+ if (!aliveRef.current) return
+ if (data.status !== 'success') return fail((data.message as string) || t('feishu_scan_fail'))
+ const rs = data.register_status as string
+ if (rs === 'done') {
+ setPhase('success')
+ await apiClient.channelAction('connect', 'feishu', {
+ feishu_app_id: data.app_id,
+ feishu_app_secret: data.app_secret,
+ })
+ if (aliveRef.current) onConnected()
+ } else if (rs === 'expired') {
+ fail(t('feishu_scan_expired'))
+ } else if (rs === 'denied') {
+ fail(t('feishu_scan_denied'))
+ } else if (rs === 'error') {
+ fail((data.message as string) || t('feishu_scan_fail'))
+ } else {
+ pollFeishu()
+ }
+ } catch {
+ pollFeishu()
+ }
+ }, POLL_INTERVAL)
+ }
+
+ const startFeishu = async () => {
+ setPhase('loading')
+ try {
+ const data = await apiClient.getFeishuRegister()
+ if (!aliveRef.current) return
+ if (data.status !== 'success') return fail(data.message || t('feishu_scan_fail'))
+ setQr(data.qr_image || data.qrcode_url || '')
+ setOpenLink(data.qrcode_url || '')
+ setPhase('waiting')
+ pollFeishu()
+ } catch {
+ fail(t('feishu_scan_fail'))
+ }
+ }
+
+ const start = () => {
+ stopPoll()
+ if (provider === 'weixin') void startWeixin()
+ else void startFeishu()
+ }
+
+ useEffect(() => {
+ aliveRef.current = true
+ start()
+ return () => {
+ aliveRef.current = false
+ stopPoll()
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [provider])
+
+ const title = provider === 'weixin' ? t('weixin_scan_title') : t('feishu_scan_title')
+ const desc = provider === 'weixin' ? t('weixin_scan_desc') : t('feishu_scan_desc')
+ const tip = provider === 'weixin' ? t('weixin_qr_tip') : t('feishu_scan_tip')
+
+ const statusText = (): string => {
+ if (provider === 'weixin') {
+ if (phase === 'scanned') return t('weixin_scan_scanned')
+ return t('weixin_scan_waiting')
+ }
+ return t('feishu_scan_waiting')
+ }
+
+ return (
+
+
+ {phase === 'loading' && (
+
+
+ {provider === 'weixin' ? t('weixin_scan_loading') : t('feishu_scan_loading')}
+
+ )}
+
+ {(phase === 'waiting' || phase === 'scanned') && (
+ <>
+
{desc}
+
+ {qr ? (
+
+ ) : (
+
QR
+ )}
+
+
{statusText()}
+
{tip}
+ {openLink && (
+
+ {t('feishu_scan_open_link')}
+
+ )}
+ >
+ )}
+
+ {phase === 'success' && (
+
+
+
+
+
+ {provider === 'weixin' ? t('weixin_scan_success') : t('feishu_scan_success')}
+
+
+ )}
+
+ {phase === 'error' && (
+
+
{errMsg}
+
+
+ {t('feishu_scan_retry')}
+
+
+ )}
+
+
+ )
+}
+
+export default QrLoginModal
diff --git a/desktop/src/renderer/src/components/StatusScreen.tsx b/desktop/src/renderer/src/components/StatusScreen.tsx
new file mode 100644
index 00000000..553b2858
--- /dev/null
+++ b/desktop/src/renderer/src/components/StatusScreen.tsx
@@ -0,0 +1,58 @@
+import React from 'react'
+import { t } from '../i18n'
+
+interface StatusScreenProps {
+ status: 'connecting' | 'error'
+ error?: string
+ onRetry: () => void
+}
+
+const StatusScreen: React.FC = ({ status, error, onRetry }) => {
+ return (
+
+
+
+
+ {status === 'connecting' && (
+ <>
+
+
+ {t('status_starting')}
+
+
+ {t('status_starting_desc')}
+
+
+
+
+
+
+
+ >
+ )}
+
+ {status === 'error' && (
+ <>
+
+
+ {t('status_error')}
+
+
+ {error || t('status_error_desc')}
+
+
+
+
+ {t('status_retry')}
+
+ >
+ )}
+
+
+ )
+}
+
+export default StatusScreen
diff --git a/desktop/src/renderer/src/components/UpdateBanner.tsx b/desktop/src/renderer/src/components/UpdateBanner.tsx
new file mode 100644
index 00000000..8c95ac10
--- /dev/null
+++ b/desktop/src/renderer/src/components/UpdateBanner.tsx
@@ -0,0 +1,97 @@
+import React, { useEffect, useState } from 'react'
+import { Download, RefreshCw, X, Loader2 } from 'lucide-react'
+import { t } from '../i18n'
+import { useUpdateStore, hasPendingUpdate } from '../store/updateStore'
+
+// Compact update panel anchored to the NavRail footer. Only mounts content
+// when there's a pending update; otherwise renders nothing so it stays out of
+// the way until electron-updater reports a new version.
+const UpdateBanner: React.FC = () => {
+ const state = useUpdateStore()
+ const [open, setOpen] = useState(false)
+
+ const pending = hasPendingUpdate(state)
+ const status = state.status
+
+ // Auto-open the panel the moment a new version is first detected.
+ useEffect(() => {
+ if (status?.state === 'available') setOpen(true)
+ }, [status?.state])
+
+ if (!pending) return null
+
+ const version = state.version
+ const downloading = status?.state === 'downloading'
+ const downloaded = status?.state === 'downloaded'
+
+ return (
+
+ {/* Collapsed pill: a red-dotted button that re-opens the panel. */}
+ {!open && (
+
setOpen(true)}
+ className="relative w-full flex items-center gap-2 rounded-btn bg-accent-soft text-accent px-3 py-2 text-[13px] font-medium cursor-pointer hover:bg-accent-soft/80 transition-colors"
+ >
+
+
+ {t('update_available')}
+
+ )}
+
+ {open && (
+
+
+
+
{t('update_available')}
+ {version &&
v{version}
}
+
+
{
+ setOpen(false)
+ state.dismiss()
+ }}
+ className="text-content-tertiary hover:text-content cursor-pointer flex-shrink-0"
+ title={t('update_later')}
+ >
+
+
+
+
+ {downloading && (
+
+
+
+ {t('update_downloading')} {state.percent}%
+
+
+
+ )}
+
+ {!downloading && !downloaded && (
+
state.download()}
+ className="w-full inline-flex items-center justify-center gap-2 rounded-btn bg-accent text-accent-contrast hover:bg-accent-hover px-3 py-2 text-[13px] font-medium cursor-pointer transition-colors"
+ >
+
+ {t('update_download')}
+
+ )}
+
+ {downloaded && (
+
state.install()}
+ className="w-full inline-flex items-center justify-center gap-2 rounded-btn bg-accent text-accent-contrast hover:bg-accent-hover px-3 py-2 text-[13px] font-medium cursor-pointer transition-colors"
+ >
+
+ {t('update_restart')}
+
+ )}
+
+ )}
+
+ )
+}
+
+export default UpdateBanner
diff --git a/desktop/src/renderer/src/highlight.css b/desktop/src/renderer/src/highlight.css
new file mode 100644
index 00000000..4391c026
--- /dev/null
+++ b/desktop/src/renderer/src/highlight.css
@@ -0,0 +1,189 @@
+/* highlight.js github themes, scoped for light/dark. Generated; do not edit. */
+pre code.hljs {
+ display: block;
+ overflow-x: auto;
+ padding: 1em
+}
+code.hljs {
+ padding: 3px 5px
+}
+/*!
+ Theme: GitHub
+ Description: Light theme as seen on github.com
+ Author: github.com
+ Maintainer: @Hirse
+ Updated: 2021-05-15
+
+ Outdated base version: https://github.com/primer/github-syntax-light
+ Current colors taken from GitHub's CSS
+*/
+.hljs {
+ color: #24292e;
+ background: #ffffff
+}
+.hljs-doctag,
+.hljs-keyword,
+.hljs-meta .hljs-keyword,
+.hljs-template-tag,
+.hljs-template-variable,
+.hljs-type,
+.hljs-variable.language_ {
+ /* prettylights-syntax-keyword */
+ color: #d73a49
+}
+.hljs-title,
+.hljs-title.class_,
+.hljs-title.class_.inherited__,
+.hljs-title.function_ {
+ /* prettylights-syntax-entity */
+ color: #6f42c1
+}
+.hljs-attr,
+.hljs-attribute,
+.hljs-literal,
+.hljs-meta,
+.hljs-number,
+.hljs-operator,
+.hljs-variable,
+.hljs-selector-attr,
+.hljs-selector-class,
+.hljs-selector-id {
+ /* prettylights-syntax-constant */
+ color: #005cc5
+}
+.hljs-regexp,
+.hljs-string,
+.hljs-meta .hljs-string {
+ /* prettylights-syntax-string */
+ color: #032f62
+}
+.hljs-built_in,
+.hljs-symbol {
+ /* prettylights-syntax-variable */
+ color: #e36209
+}
+.hljs-comment,
+.hljs-code,
+.hljs-formula {
+ /* prettylights-syntax-comment */
+ color: #6a737d
+}
+.hljs-name,
+.hljs-quote,
+.hljs-selector-tag,
+.hljs-selector-pseudo {
+ /* prettylights-syntax-entity-tag */
+ color: #22863a
+}
+.hljs-subst {
+ /* prettylights-syntax-storage-modifier-import */
+ color: #24292e
+}
+.hljs-section {
+ /* prettylights-syntax-markup-heading */
+ color: #005cc5;
+ font-weight: bold
+}
+.hljs-bullet {
+ /* prettylights-syntax-markup-list */
+ color: #735c0f
+}
+.hljs-emphasis {
+ /* prettylights-syntax-markup-italic */
+ color: #24292e;
+ font-style: italic
+}
+.hljs-strong {
+ /* prettylights-syntax-markup-bold */
+ color: #24292e;
+ font-weight: bold
+}
+.hljs-addition {
+ /* prettylights-syntax-markup-inserted */
+ color: #22863a;
+ background-color: #f0fff4
+}
+.hljs-deletion {
+ /* prettylights-syntax-markup-deleted */
+ color: #b31d28;
+ background-color: #ffeef0
+}
+.hljs-char.escape_,
+.hljs-link,
+.hljs-params,
+.hljs-property,
+.hljs-punctuation,
+.hljs-tag {
+ /* purposely ignored */
+
+}
+.dark pre code.hljs {
+ display: block;
+ overflow-x: auto;
+ padding: 1em
+}.dark code.hljs {
+ padding: 3px 5px
+}.dark /*!
+ Theme: GitHub Dark
+ Description: Dark theme as seen on github.com
+ Author: github.com
+ Maintainer: @Hirse
+ Updated: 2021-05-15
+
+ Outdated base version: https://github.com/primer/github-syntax-dark
+ Current colors taken from GitHub's CSS
+*/
+.hljs {
+ color: #c9d1d9;
+ background: #0d1117
+}.dark .hljs-doctag, .dark .hljs-keyword, .dark .hljs-meta .hljs-keyword, .dark .hljs-template-tag, .dark .hljs-template-variable, .dark .hljs-type, .dark .hljs-variable.language_ {
+ /* prettylights-syntax-keyword */
+ color: #ff7b72
+}.dark .hljs-title, .dark .hljs-title.class_, .dark .hljs-title.class_.inherited__, .dark .hljs-title.function_ {
+ /* prettylights-syntax-entity */
+ color: #d2a8ff
+}.dark .hljs-attr, .dark .hljs-attribute, .dark .hljs-literal, .dark .hljs-meta, .dark .hljs-number, .dark .hljs-operator, .dark .hljs-variable, .dark .hljs-selector-attr, .dark .hljs-selector-class, .dark .hljs-selector-id {
+ /* prettylights-syntax-constant */
+ color: #79c0ff
+}.dark .hljs-regexp, .dark .hljs-string, .dark .hljs-meta .hljs-string {
+ /* prettylights-syntax-string */
+ color: #a5d6ff
+}.dark .hljs-built_in, .dark .hljs-symbol {
+ /* prettylights-syntax-variable */
+ color: #ffa657
+}.dark .hljs-comment, .dark .hljs-code, .dark .hljs-formula {
+ /* prettylights-syntax-comment */
+ color: #8b949e
+}.dark .hljs-name, .dark .hljs-quote, .dark .hljs-selector-tag, .dark .hljs-selector-pseudo {
+ /* prettylights-syntax-entity-tag */
+ color: #7ee787
+}.dark .hljs-subst {
+ /* prettylights-syntax-storage-modifier-import */
+ color: #c9d1d9
+}.dark .hljs-section {
+ /* prettylights-syntax-markup-heading */
+ color: #1f6feb;
+ font-weight: bold
+}.dark .hljs-bullet {
+ /* prettylights-syntax-markup-list */
+ color: #f2cc60
+}.dark .hljs-emphasis {
+ /* prettylights-syntax-markup-italic */
+ color: #c9d1d9;
+ font-style: italic
+}.dark .hljs-strong {
+ /* prettylights-syntax-markup-bold */
+ color: #c9d1d9;
+ font-weight: bold
+}.dark .hljs-addition {
+ /* prettylights-syntax-markup-inserted */
+ color: #aff5b4;
+ background-color: #033a16
+}.dark .hljs-deletion {
+ /* prettylights-syntax-markup-deleted */
+ color: #ffdcd7;
+ background-color: #67060c
+}.dark .hljs-char.escape_, .dark .hljs-link, .dark .hljs-params, .dark .hljs-property, .dark .hljs-punctuation, .dark .hljs-tag {
+ /* purposely ignored */
+
+}
diff --git a/desktop/src/renderer/src/hooks/useBackend.ts b/desktop/src/renderer/src/hooks/useBackend.ts
new file mode 100644
index 00000000..146ee071
--- /dev/null
+++ b/desktop/src/renderer/src/hooks/useBackend.ts
@@ -0,0 +1,137 @@
+import { useState, useEffect, useCallback, useRef } from 'react'
+
+interface BackendState {
+ status: 'connecting' | 'ready' | 'error'
+ port: number
+ error?: string
+}
+
+export function useBackend() {
+ const [state, setState] = useState({
+ status: 'connecting',
+ port: 9899,
+ })
+ const pollingRef = useRef | null>(null)
+
+ const probeBackend = useCallback(async (port: number): Promise => {
+ try {
+ const res = await fetch(`http://127.0.0.1:${port}/config`, {
+ signal: AbortSignal.timeout(3000),
+ })
+ return res.ok
+ } catch {
+ return false
+ }
+ }, [])
+
+ // True once the backend has answered at least once. After this we never flip
+ // back to "error" from polling — a hidden/backgrounded window throttles JS
+ // timers, so attempt counters are unreliable and would otherwise produce a
+ // false "failed to start" even though the backend is alive.
+ const readyRef = useRef(false)
+ // Holds the latest resolved port so the visibility handler (registered once)
+ // always probes the correct port without re-running the effect.
+ const portRef = useRef(9899)
+
+ useEffect(() => {
+ let cancelled = false
+ let offStatus: (() => void) | undefined
+ const api = window.electronAPI
+
+ // Use a wall-clock deadline instead of an attempt counter so timer
+ // throttling (when the window is in the background) can't fast-forward us
+ // into a false failure. Only give up if we genuinely can't reach the
+ // backend for this long.
+ const startPolling = async (port: number) => {
+ portRef.current = port
+ const deadline = Date.now() + 90_000
+
+ const poll = async () => {
+ if (cancelled) return
+
+ const ready = await probeBackend(port)
+ if (cancelled) return
+
+ if (ready) {
+ readyRef.current = true
+ setState({ status: 'ready', port })
+ return
+ }
+
+ // Backend already answered before but is briefly unreachable (e.g.
+ // window was asleep): keep retrying, never surface an error.
+ if (!readyRef.current && Date.now() >= deadline) {
+ // Leave error undefined so StatusScreen shows the localized,
+ // user-friendly message instead of a raw technical string.
+ setState({ status: 'error', port })
+ return
+ }
+
+ pollingRef.current = setTimeout(poll, 1000)
+ }
+
+ await poll()
+ }
+
+ if (api) {
+ api.getBackendPort().then((port) => {
+ const p = port || 9899
+ portRef.current = p
+ setState((prev) => ({ ...prev, port: p }))
+ startPolling(p)
+ })
+
+ offStatus = api.onBackendStatus((data) => {
+ if (data.status === 'ready' && data.port) {
+ readyRef.current = true
+ portRef.current = data.port
+ setState({ status: 'ready', port: data.port })
+ if (pollingRef.current) {
+ clearTimeout(pollingRef.current)
+ pollingRef.current = null
+ }
+ } else if (data.status === 'error' && !readyRef.current) {
+ // Ignore late "error" from the main process once we've been ready —
+ // it usually means the window was backgrounded, not a real failure.
+ // Drop the raw technical message; StatusScreen shows a localized one.
+ setState((prev) => ({ ...prev, status: 'error' }))
+ }
+ })
+ } else {
+ startPolling(9899)
+ }
+
+ // When the window comes back to the foreground, re-probe immediately so a
+ // user returning after a while sees the real (ready) state right away
+ // instead of waiting for the throttled timer to catch up.
+ const onVisible = () => {
+ if (cancelled || document.visibilityState !== 'visible') return
+ probeBackend(portRef.current).then((ready) => {
+ if (cancelled || !ready) return
+ readyRef.current = true
+ setState((prev) => ({ ...prev, status: 'ready' }))
+ })
+ }
+ document.addEventListener('visibilitychange', onVisible)
+
+ return () => {
+ cancelled = true
+ if (pollingRef.current) {
+ clearTimeout(pollingRef.current)
+ }
+ offStatus?.()
+ document.removeEventListener('visibilitychange', onVisible)
+ }
+ }, [probeBackend])
+
+ const restart = useCallback(async () => {
+ setState((prev) => ({ ...prev, status: 'connecting', error: undefined }))
+ if (window.electronAPI) {
+ await window.electronAPI.restartBackend()
+ }
+ }, [])
+
+ const baseUrl = `http://127.0.0.1:${state.port}`
+
+ return { ...state, baseUrl, restart }
+}
diff --git a/desktop/src/renderer/src/hooks/usePlatform.ts b/desktop/src/renderer/src/hooks/usePlatform.ts
new file mode 100644
index 00000000..b99dbe05
--- /dev/null
+++ b/desktop/src/renderer/src/hooks/usePlatform.ts
@@ -0,0 +1,29 @@
+import { useEffect, useState } from 'react'
+
+export type Platform = 'mac' | 'win' | 'linux'
+
+function detectPlatform(): Platform {
+ const p = window.electronAPI?.platform
+ if (p === 'darwin') return 'mac'
+ if (p === 'win32') return 'win'
+ if (p === 'linux') return 'linux'
+ // Fallback for browser dev without electron
+ if (typeof navigator !== 'undefined' && /Mac/i.test(navigator.platform)) return 'mac'
+ return 'win'
+}
+
+/**
+ * Resolves the host platform and applies a `.platform-*` class on
+ * so CSS can branch on platform (titlebar layout, scrollbars, etc.).
+ */
+export function usePlatform(): { platform: Platform; isMac: boolean; isWin: boolean } {
+ const [platform] = useState(detectPlatform)
+
+ useEffect(() => {
+ const root = document.documentElement
+ root.classList.remove('platform-mac', 'platform-win', 'platform-linux')
+ root.classList.add(`platform-${platform}`)
+ }, [platform])
+
+ return { platform, isMac: platform === 'mac', isWin: platform === 'win' }
+}
diff --git a/desktop/src/renderer/src/hooks/useTheme.ts b/desktop/src/renderer/src/hooks/useTheme.ts
new file mode 100644
index 00000000..47f19270
--- /dev/null
+++ b/desktop/src/renderer/src/hooks/useTheme.ts
@@ -0,0 +1,57 @@
+import { useState, useEffect, useCallback } from 'react'
+
+export type ThemePref = 'light' | 'dark' | 'system'
+export type ResolvedTheme = 'light' | 'dark'
+
+const STORAGE_KEY = 'cow_theme'
+
+function getSystemTheme(): ResolvedTheme {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
+}
+
+function readStored(): ThemePref {
+ const saved = localStorage.getItem(STORAGE_KEY)
+ if (saved === 'dark' || saved === 'light' || saved === 'system') return saved
+ // Default to dark to match the app's flagship look
+ return 'dark'
+}
+
+function applyTheme(resolved: ResolvedTheme) {
+ const root = document.documentElement
+ root.classList.toggle('dark', resolved === 'dark')
+}
+
+export function useTheme() {
+ const [pref, setPref] = useState(readStored)
+ const [resolved, setResolved] = useState(() =>
+ readStored() === 'system' ? getSystemTheme() : (readStored() as ResolvedTheme)
+ )
+
+ useEffect(() => {
+ const next: ResolvedTheme = pref === 'system' ? getSystemTheme() : pref
+ setResolved(next)
+ applyTheme(next)
+ localStorage.setItem(STORAGE_KEY, pref)
+ }, [pref])
+
+ // Follow system changes only when preference is "system"
+ useEffect(() => {
+ if (pref !== 'system') return
+ const mq = window.matchMedia('(prefers-color-scheme: dark)')
+ const handler = () => {
+ const next = getSystemTheme()
+ setResolved(next)
+ applyTheme(next)
+ }
+ mq.addEventListener('change', handler)
+ return () => mq.removeEventListener('change', handler)
+ }, [pref])
+
+ const toggleTheme = useCallback(() => {
+ setPref(resolved === 'dark' ? 'light' : 'dark')
+ }, [resolved])
+
+ const setTheme = useCallback((next: ThemePref) => setPref(next), [])
+
+ return { theme: resolved, pref, toggleTheme, setTheme }
+}
diff --git a/desktop/src/renderer/src/i18n.ts b/desktop/src/renderer/src/i18n.ts
new file mode 100644
index 00000000..3beb4ba3
--- /dev/null
+++ b/desktop/src/renderer/src/i18n.ts
@@ -0,0 +1,627 @@
+const translations: Record> = {
+ zh: {
+ console: '控制台',
+ nav_chat: '对话',
+ nav_manage: '管理',
+ nav_monitor: '监控',
+ menu_chat: '对话',
+ menu_config: '配置',
+ menu_skills: '技能',
+ menu_memory: '记忆',
+ menu_channels: '通道',
+ menu_tasks: '定时',
+ menu_logs: '日志',
+ menu_models: '模型',
+ menu_knowledge: '知识',
+ menu_settings: '设置',
+ // knowledge
+ knowledge_title: '知识库',
+ knowledge_desc: '浏览和探索你的知识库',
+ knowledge_tab_docs: '文档',
+ knowledge_tab_graph: '图谱',
+ knowledge_search: '搜索文档...',
+ knowledge_stats: '{pages} 篇 · {size}',
+ knowledge_select_hint: '从左侧选择一个文档查看',
+ knowledge_empty: '知识库还是空的',
+ knowledge_empty_guide: '在对话中发送文档、链接或主题给 Agent,它会自动整理到你的知识库中',
+ knowledge_go_chat: '开始对话',
+ knowledge_loading: '加载知识库中...',
+ knowledge_graph_empty: '暂无关联图谱',
+ knowledge_disabled: '知识库未启用',
+ knowledge_doc_load_error: '文档加载失败',
+ nav_expand: '展开侧栏',
+ nav_collapse: '收起侧栏',
+ update_available: '发现新版本',
+ update_download: '下载更新',
+ update_downloading: '正在下载',
+ update_restart: '重启以更新',
+ update_later: '稍后',
+ update_latest: '已是最新版本',
+ // onboarding
+ onboarding_welcome_title: '欢迎使用 CowAgent',
+ onboarding_welcome_desc: '你的私人超级 AI 助手。几步设置,即可开始对话。',
+ onboarding_lang_label: '界面语言',
+ onboarding_model_title: '配置对话模型',
+ onboarding_model_desc: '选择模型厂商并填入 API Key,即可开始使用。',
+ onboarding_provider: '模型厂商',
+ onboarding_select_provider: '选择厂商',
+ onboarding_apikey: 'API Key',
+ onboarding_apikey_placeholder: '输入 API 密钥',
+ onboarding_key_guide: '没有秘钥?前往创建',
+ onboarding_apibase: 'API 地址(可选)',
+ onboarding_model: '模型',
+ onboarding_select_model: '选择模型',
+ onboarding_done_title: '一切就绪',
+ onboarding_done_desc: '配置完成,开始你的第一次对话吧。',
+ onboarding_next: '下一步',
+ onboarding_back: '上一步',
+ onboarding_skip: '跳过',
+ onboarding_finish: '开始对话',
+ onboarding_step: '第 {n} / {total} 步',
+ onboarding_saving: '保存中...',
+ onboarding_save_failed: '保存失败,请检查后重试',
+ sessions_title: '会话',
+ session_new: '新对话',
+ session_rename: '重命名',
+ session_delete: '删除',
+ session_empty: '暂无会话',
+ session_today: '今天',
+ session_yesterday: '昨天',
+ session_earlier: '更早',
+ msg_copy: '复制',
+ msg_copied: '已复制',
+ msg_regenerate: '重新生成',
+ msg_edit: '编辑',
+ msg_delete: '删除',
+ msg_cancelled: '已中止',
+ msg_self_learned: '自主学习',
+ msg_stop: '停止',
+ chat_clear_context: '清除上下文',
+ chat_load_earlier: '加载更早的消息',
+ chat_send: '发送',
+ chat_attach: '添加附件',
+ slash_hint: '输入 / 查看命令',
+ chat_welcome: '有什么可以帮你的?',
+ chat_empty_hint: '发送一条消息开始对话',
+ welcome_subtitle: '我可以帮你解答问题、管理你的电脑、创建并执行技能,\n还能通过长期记忆不断成长。',
+ example_sys_title: '系统管理',
+ example_sys_text: '帮我查看工作空间里有哪些文件',
+ example_task_title: '技能系统',
+ example_task_text: '查看所有支持的工具和技能',
+ example_code_title: '编程助手',
+ example_code_text: '帮我编写一个Python爬虫脚本',
+ input_placeholder: '输入消息...',
+ config_title: '配置管理',
+ config_desc: '管理模型和 Agent 配置',
+ config_model: '模型配置',
+ config_agent: 'Agent 配置',
+ config_provider: '模型厂商',
+ config_model_name: '模型',
+ config_custom_model_hint: '输入自定义模型名称',
+ config_save: '保存',
+ config_saved: '已保存',
+ config_save_error: '保存失败',
+ config_custom_option: '自定义',
+ config_max_tokens: '最大上下文 Token',
+ config_max_turns: '最大记忆轮次',
+ config_max_steps: '最大执行步数',
+ config_max_tokens_hint: '对话中 Agent 能输入的最大 Token 长度,超过后会智能压缩处理',
+ config_max_turns_hint: '一问一答为一轮,超过后会智能压缩处理',
+ config_max_steps_hint: '单次对话中 Agent 最多调用工具的次数',
+ config_thinking: '深度思考',
+ config_thinking_hint: '是否启用深度思考模式',
+ config_evolution: '自主进化',
+ config_evolution_hint: '会话空闲后自动复盘,沉淀记忆、优化技能、处理未完成事项',
+ config_security: '安全设置',
+ config_password: '访问密码',
+ config_password_hint: '留空则不启用密码保护',
+ config_password_placeholder: '留空表示不设密码',
+ config_password_saved: '密码已更新,请重新登录',
+ config_password_cleared: '密码已清除',
+ config_language: '语言',
+ config_language_hint: '界面与回复语言',
+ config_credentials_link: 'API Key 与接口地址请在「模型配置」中设置',
+ config_goto_models: '前往配置',
+ config_provider_unconfigured: '未配置',
+ config_provider_unconfigured_hint: '该厂商尚未配置 API Key,请先前往配置',
+ config_cancel: '取消',
+ // settings tabs
+ settings_tab_basic: '基础配置',
+ settings_tab_models: '模型配置',
+ // models tab
+ models_vendors: '厂商凭据',
+ models_vendors_sub: '一处配置,多个模型能力共享',
+ models_configured: '已配置',
+ models_no_vendor: '尚未配置任何厂商',
+ models_add_vendor: '添加厂商',
+ models_custom_vendor: '自定义',
+ models_add_custom: '添加自定义厂商',
+ models_add_custom_hint: '接口需遵循 OpenAI API 协议',
+ models_edit_custom: '编辑自定义厂商',
+ models_custom_name: '名称',
+ models_custom_base_hint: '接口需遵循 OpenAI API 协议',
+ models_clear: '清除凭据',
+ models_delete: '删除',
+ models_clear_confirm: '确认清除该厂商的 API Key 与 Base URL 吗?相关能力将不再可用。',
+ models_delete_confirm: '确定删除该自定义厂商吗?此操作无法撤销。',
+ models_provider: '厂商',
+ models_model: '模型',
+ models_voice: '音色',
+ models_select_provider: '待选择',
+ models_select_model: '请选择模型',
+ models_select_voice: '请选择音色',
+ models_no_options: '暂无可选项',
+ models_auto: '自动',
+ models_asr_auto: '自动(跟随主模型)',
+ models_disabled: '不启用',
+ models_fallback: '兜底',
+ models_cap_chat: '主模型',
+ models_cap_chat_sub: '用于基础对话和 Agent 推理',
+ models_cap_vision: '图像理解',
+ models_cap_vision_sub: '识别图片内容,用于图像识别工具',
+ models_cap_image: '图像生成',
+ models_cap_image_sub: '生成图片,用于图像生成技能',
+ models_cap_asr: '语音识别',
+ models_cap_asr_sub: '语音转文字',
+ models_cap_tts: '语音合成',
+ models_cap_tts_sub: '文字转语音',
+ models_cap_embedding: '向量',
+ models_cap_embedding_sub: '用于记忆与知识的向量化检索',
+ models_cap_search: '联网搜索',
+ models_cap_search_sub: '实时网页检索能力,用于搜索工具',
+ models_tts_reply_mode: '语音回复模式',
+ models_tts_reply_mode_hint: '决定何时以语音回复用户',
+ models_tts_mode_off: '关闭',
+ models_tts_mode_if_voice: '仅当用户发语音时',
+ models_tts_mode_always: '始终语音回复',
+ models_embedding_dim: '维度',
+ models_embedding_rebuild_hint: '切换向量模型后,已有索引将失效,需要重建',
+ models_search_strategy: '策略',
+ models_search_auto: '自动',
+ models_search_fixed: '指定',
+ models_search_provider: '搜索厂商',
+ models_search_bocha_key: '配置博查 API Key',
+ models_search_bocha_hint: '前往博查开放平台创建 API Key',
+ skills_title: '技能管理',
+ skills_desc: '查看、启用或禁用 Agent 工具和技能',
+ skills_hub_btn: '探索技能广场',
+ tools_section_title: '内置工具',
+ skills_section_title: '技能',
+ tools_loading: '加载工具中...',
+ skills_loading: '加载技能中...',
+ skills_loading_desc: '技能将在加载后显示',
+ skill_enabled: '已启用',
+ skill_disabled: '已禁用',
+ tools_empty: '暂无内置工具',
+ skills_empty: '暂无技能',
+ memory_title: '记忆管理',
+ memory_desc: '查看 Agent 记忆文件和内容',
+ memory_tab_files: '记忆文件',
+ memory_tab_dreams: '自主进化',
+ memory_loading: '加载记忆文件中...',
+ memory_loading_desc: '记忆文件将在加载后显示',
+ memory_col_name: '文件名',
+ memory_col_type: '类型',
+ memory_col_size: '大小',
+ memory_col_updated: '更新时间',
+ memory_back: '返回列表',
+ memory_empty_files: '暂无记忆文件',
+ memory_empty_evolution: '暂无进化记录',
+ memory_type_global: '全局',
+ memory_type_daily: '每日',
+ memory_type_evolution: '进化',
+ memory_type_dream: '梦境',
+ memory_doc_load_error: '内容加载失败',
+ memory_prev: '上一页',
+ memory_next: '下一页',
+ channels_title: '通道管理',
+ channels_desc: '查看和管理消息通道',
+ channels_add: '添加通道',
+ channels_connected: '已连接',
+ channels_disconnected: '未连接',
+ channels_connect: '连接',
+ channels_disconnect: '断开',
+ channels_save: '保存',
+ channels_loading: '加载通道中...',
+ channels_connected_section: '已连接',
+ channels_available_section: '可添加',
+ channels_empty_connected: '暂无已连接的通道',
+ channels_qr_hint: '该通道通过扫码登录,请前往 Web 控制台完成扫码连接',
+ channels_save_ok: '已保存',
+ channels_save_error: '保存失败',
+ channels_connect_error: '连接失败',
+ channels_scan_login: '扫码登录',
+ channels_scan_register: '扫码注册',
+ weixin_scan_title: '微信扫码登录',
+ weixin_scan_desc: '使用微信扫描下方二维码登录',
+ weixin_scan_loading: '正在获取二维码...',
+ weixin_scan_waiting: '等待扫码',
+ weixin_scan_scanned: '已扫描,请在手机上确认',
+ weixin_scan_success: '登录成功',
+ weixin_scan_expired: '二维码已过期,正在刷新...',
+ weixin_scan_fail: '获取二维码失败',
+ weixin_qr_tip: '请使用登录的微信扫码',
+ feishu_scan_title: '飞书扫码注册',
+ feishu_scan_desc: '扫码后将自动创建并连接飞书机器人',
+ feishu_scan_loading: '正在生成二维码...',
+ feishu_scan_waiting: '请使用飞书扫码授权',
+ feishu_scan_tip: '扫码后在飞书中确认授权',
+ feishu_scan_open_link: '打开授权链接',
+ feishu_scan_success: '注册成功',
+ feishu_scan_expired: '二维码已过期',
+ feishu_scan_denied: '授权被拒绝',
+ feishu_scan_fail: '注册失败',
+ feishu_scan_retry: '重试',
+ tasks_title: '定时任务',
+ tasks_desc: '查看和管理定时任务',
+ tasks_active: '运行中',
+ tasks_paused: '已暂停',
+ tasks_empty: '暂无定时任务',
+ tasks_empty_guide: '在对话中告诉 Agent「每天 9 点提醒我…」即可创建定时任务',
+ tasks_go_chat: '去对话创建',
+ tasks_next_run: '下次执行',
+ tasks_loading: '加载定时任务中...',
+ task_edit_title: '编辑任务',
+ task_name: '名称',
+ task_enabled: '启用',
+ task_schedule_type: '调度类型',
+ task_type_cron: 'Cron 表达式',
+ task_type_interval: '固定间隔',
+ task_type_once: '单次执行',
+ task_cron_expr: 'Cron 表达式',
+ task_cron_hint: '如 0 9 * * * 表示每天 9 点',
+ task_interval_seconds: '间隔(秒)',
+ task_once_time: '执行时间',
+ task_action_type: '动作类型',
+ task_action_send: '发送消息',
+ task_action_agent: 'Agent 任务',
+ task_message_content: '消息内容',
+ task_task_description: '任务描述',
+ task_channel: '通道',
+ task_receiver: '接收者',
+ task_channel_locked: '通道与接收者创建后不可修改',
+ task_save: '保存',
+ task_cancel: '取消',
+ task_delete: '删除',
+ task_delete_confirm: '确定删除该任务吗?此操作不可撤销。',
+ task_save_error: '保存失败',
+ logs_title: '日志',
+ logs_desc: '实时日志输出 (run.log)',
+ logs_live: '实时',
+ logs_connecting: '日志流将在连接后显示...',
+ status_starting: '正在启动 CowAgent...',
+ status_starting_desc: '正在初始化客户端,请稍候',
+ status_error: '初始化失败',
+ status_error_desc: '客户端初始化失败,请重试',
+ status_retry: '重试',
+ },
+ en: {
+ console: 'Console',
+ nav_chat: 'Chat',
+ nav_manage: 'Management',
+ nav_monitor: 'Monitor',
+ menu_chat: 'Chat',
+ menu_config: 'Config',
+ menu_skills: 'Skills',
+ menu_memory: 'Memory',
+ menu_channels: 'Channels',
+ menu_tasks: 'Tasks',
+ menu_logs: 'Logs',
+ menu_models: 'Models',
+ menu_knowledge: 'Knowledge',
+ // knowledge
+ knowledge_title: 'Knowledge Base',
+ knowledge_desc: 'Browse and explore your knowledge base',
+ knowledge_tab_docs: 'Documents',
+ knowledge_tab_graph: 'Graph',
+ knowledge_search: 'Search documents...',
+ knowledge_stats: '{pages} pages · {size}',
+ knowledge_select_hint: 'Select a document to view',
+ knowledge_empty: 'Your knowledge base is empty',
+ knowledge_empty_guide: 'Send documents, links or topics to the agent in chat — it will organize them into your knowledge base',
+ knowledge_go_chat: 'Start chatting',
+ knowledge_loading: 'Loading knowledge base...',
+ knowledge_graph_empty: 'No graph available',
+ knowledge_disabled: 'Knowledge base is disabled',
+ knowledge_doc_load_error: 'Failed to load document',
+ menu_settings: 'Settings',
+ nav_expand: 'Expand sidebar',
+ nav_collapse: 'Collapse sidebar',
+ update_available: 'New version available',
+ update_download: 'Download update',
+ update_downloading: 'Downloading',
+ update_restart: 'Restart to update',
+ update_later: 'Later',
+ update_latest: 'You are up to date',
+ // onboarding
+ onboarding_welcome_title: 'Welcome to CowAgent',
+ onboarding_welcome_desc: 'Your personal super AI assistant. A few quick steps and you are ready to chat.',
+ onboarding_lang_label: 'Language',
+ onboarding_model_title: 'Set up your chat model',
+ onboarding_model_desc: 'Pick a provider and paste its API key to get started.',
+ onboarding_provider: 'Provider',
+ onboarding_select_provider: 'Select a provider',
+ onboarding_apikey: 'API Key',
+ onboarding_apikey_placeholder: 'Enter your API key',
+ onboarding_key_guide: 'No key yet? Create one',
+ onboarding_apibase: 'API base (optional)',
+ onboarding_model: 'Model',
+ onboarding_select_model: 'Select a model',
+ onboarding_done_title: 'All set',
+ onboarding_done_desc: 'Setup complete. Start your first conversation.',
+ onboarding_next: 'Next',
+ onboarding_back: 'Back',
+ onboarding_skip: 'Skip',
+ onboarding_finish: 'Start chatting',
+ onboarding_step: 'Step {n} of {total}',
+ onboarding_saving: 'Saving...',
+ onboarding_save_failed: 'Save failed, please check and retry',
+ sessions_title: 'Chats',
+ session_new: 'New chat',
+ session_rename: 'Rename',
+ session_delete: 'Delete',
+ session_empty: 'No conversations yet',
+ session_today: 'Today',
+ session_yesterday: 'Yesterday',
+ session_earlier: 'Earlier',
+ msg_copy: 'Copy',
+ msg_copied: 'Copied',
+ msg_regenerate: 'Regenerate',
+ msg_edit: 'Edit',
+ msg_delete: 'Delete',
+ msg_cancelled: 'Cancelled',
+ msg_self_learned: 'Self-learned',
+ msg_stop: 'Stop',
+ chat_clear_context: 'Clear context',
+ chat_load_earlier: 'Load earlier messages',
+ chat_send: 'Send',
+ chat_attach: 'Attach file',
+ slash_hint: 'Type / for commands',
+ chat_welcome: 'How can I help you?',
+ chat_empty_hint: 'Send a message to start the conversation',
+ welcome_subtitle: 'I can help you answer questions, manage your computer, create and execute skills,\nand keep growing through long-term memory.',
+ example_sys_title: 'System',
+ example_sys_text: 'Show me the files in the workspace',
+ example_task_title: 'Skills',
+ example_task_text: 'Show current tools and skills',
+ example_code_title: 'Coding',
+ example_code_text: 'Write a Python web scraper script',
+ input_placeholder: 'Type a message...',
+ config_title: 'Configuration',
+ config_desc: 'Manage model and agent settings',
+ config_model: 'Model Configuration',
+ config_agent: 'Agent Configuration',
+ config_provider: 'Provider',
+ config_model_name: 'Model',
+ config_custom_model_hint: 'Enter custom model name',
+ config_save: 'Save',
+ config_saved: 'Saved',
+ config_save_error: 'Save failed',
+ config_custom_option: 'Custom',
+ config_max_tokens_hint: 'Max token length the Agent can take in; longer context is compressed automatically',
+ config_max_turns_hint: 'One question and answer is a turn; older turns are compressed automatically',
+ config_max_steps_hint: 'Max tool calls the Agent can make in one turn',
+ config_thinking: 'Deep Thinking',
+ config_thinking_hint: 'Whether to enable deep thinking mode',
+ config_evolution: 'Self-Evolution',
+ config_evolution_hint: 'Review automatically when idle: consolidate memory, refine skills, finish pending tasks',
+ config_security: 'Security',
+ config_password: 'Access Password',
+ config_password_hint: 'Leave empty to disable password protection',
+ config_password_placeholder: 'Leave empty for no password',
+ config_password_saved: 'Password updated, please log in again',
+ config_password_cleared: 'Password cleared',
+ config_language: 'Language',
+ config_language_hint: 'Interface and reply language',
+ config_credentials_link: 'Set API key and endpoint in "Models"',
+ config_goto_models: 'Configure',
+ config_provider_unconfigured: 'Not configured',
+ config_provider_unconfigured_hint: 'This provider has no API key yet — configure it first',
+ config_cancel: 'Cancel',
+ // settings tabs
+ settings_tab_basic: 'Basic',
+ settings_tab_models: 'Models',
+ // models tab
+ models_vendors: 'Provider Credentials',
+ models_vendors_sub: 'Configured once, shared by multiple model capabilities',
+ models_configured: 'configured',
+ models_no_vendor: 'No provider configured yet',
+ models_add_vendor: 'Add Provider',
+ models_custom_vendor: 'Custom',
+ models_add_custom: 'Add custom provider',
+ models_add_custom_hint: 'Endpoint must follow the OpenAI API protocol',
+ models_edit_custom: 'Edit custom provider',
+ models_custom_name: 'Name',
+ models_custom_base_hint: 'Endpoint must follow the OpenAI API protocol',
+ models_clear: 'Clear credentials',
+ models_delete: 'Delete',
+ models_clear_confirm: 'Clear the API key and base URL for this provider? Related capabilities will stop working.',
+ models_delete_confirm: 'Delete this custom provider? This cannot be undone.',
+ models_provider: 'Provider',
+ models_model: 'Model',
+ models_voice: 'Voice',
+ models_select_provider: 'Select',
+ models_select_model: 'Select a model',
+ models_select_voice: 'Select a voice',
+ models_no_options: 'No options',
+ models_auto: 'Auto',
+ models_asr_auto: 'Auto (follow main model)',
+ models_disabled: 'Disabled',
+ models_fallback: 'Fallback',
+ models_cap_chat: 'Main Model',
+ models_cap_chat_sub: 'Used for basic chat and agent reasoning',
+ models_cap_vision: 'Image Understanding',
+ models_cap_vision_sub: 'Recognizes image content, used by image recognition tools',
+ models_cap_image: 'Image Generation',
+ models_cap_image_sub: 'Generates images, used by image generation skills',
+ models_cap_asr: 'Speech Recognition',
+ models_cap_asr_sub: 'Voice to text',
+ models_cap_tts: 'Speech Synthesis',
+ models_cap_tts_sub: 'Text to voice',
+ models_cap_embedding: 'Embedding',
+ models_cap_embedding_sub: 'Used for vectorized retrieval of memory and knowledge',
+ models_cap_search: 'Web Search',
+ models_cap_search_sub: 'Real-time web retrieval, used by search tools',
+ models_tts_reply_mode: 'Voice Reply Mode',
+ models_tts_reply_mode_hint: 'When to reply with voice',
+ models_tts_mode_off: 'Off',
+ models_tts_mode_if_voice: 'Only when user sends voice',
+ models_tts_mode_always: 'Always reply with voice',
+ models_embedding_dim: 'Dimension',
+ models_embedding_rebuild_hint: 'Existing index becomes invalid and must be rebuilt after changing the model',
+ models_search_strategy: 'Strategy',
+ models_search_auto: 'Auto',
+ models_search_fixed: 'Pinned',
+ models_search_provider: 'Search provider',
+ models_search_bocha_key: 'Configure Bocha API Key',
+ models_search_bocha_hint: 'Create a key at the Bocha open platform',
+ config_max_tokens: 'Max Context Tokens',
+ config_max_turns: 'Max Memory Turns',
+ config_max_steps: 'Max Steps',
+ skills_title: 'Skills',
+ skills_desc: 'View, enable, or disable agent tools and skills',
+ skills_hub_btn: 'Skill Hub',
+ tools_section_title: 'Built-in Tools',
+ skills_section_title: 'Skills',
+ tools_loading: 'Loading tools...',
+ skills_loading: 'Loading skills...',
+ skills_loading_desc: 'Skills will be displayed here after loading',
+ skill_enabled: 'Enabled',
+ skill_disabled: 'Disabled',
+ tools_empty: 'No built-in tools',
+ skills_empty: 'No skills found',
+ memory_title: 'Memory',
+ memory_desc: 'View agent memory files and contents',
+ memory_tab_files: 'Memory Files',
+ memory_tab_dreams: 'Self-Evolution',
+ memory_loading: 'Loading memory files...',
+ memory_loading_desc: 'Memory files will be displayed here',
+ memory_col_name: 'Filename',
+ memory_col_type: 'Type',
+ memory_col_size: 'Size',
+ memory_col_updated: 'Updated',
+ memory_back: 'Back to list',
+ memory_empty_files: 'No memory files',
+ memory_empty_evolution: 'No evolution records yet',
+ memory_type_global: 'Global',
+ memory_type_daily: 'Daily',
+ memory_type_evolution: 'Evolution',
+ memory_type_dream: 'Dream',
+ memory_doc_load_error: 'Failed to load content',
+ memory_prev: 'Prev',
+ memory_next: 'Next',
+ channels_title: 'Channels',
+ channels_desc: 'View and manage messaging channels',
+ channels_add: 'Add channel',
+ channels_connected: 'Connected',
+ channels_disconnected: 'Disconnected',
+ channels_connect: 'Connect',
+ channels_disconnect: 'Disconnect',
+ channels_save: 'Save',
+ channels_loading: 'Loading channels...',
+ channels_connected_section: 'Connected',
+ channels_available_section: 'Available',
+ channels_empty_connected: 'No connected channels yet',
+ channels_qr_hint: 'This channel uses QR login — please connect it from the Web console',
+ channels_save_ok: 'Saved',
+ channels_save_error: 'Failed to save',
+ channels_connect_error: 'Failed to connect',
+ channels_scan_login: 'QR login',
+ channels_scan_register: 'QR register',
+ weixin_scan_title: 'WeChat QR Login',
+ weixin_scan_desc: 'Scan the QR code below with WeChat to log in',
+ weixin_scan_loading: 'Fetching QR code...',
+ weixin_scan_waiting: 'Waiting for scan',
+ weixin_scan_scanned: 'Scanned, please confirm on your phone',
+ weixin_scan_success: 'Logged in',
+ weixin_scan_expired: 'QR code expired, refreshing...',
+ weixin_scan_fail: 'Failed to fetch QR code',
+ weixin_qr_tip: 'Scan with the WeChat account to log in',
+ feishu_scan_title: 'Feishu QR Register',
+ feishu_scan_desc: 'Scanning will create and connect a Feishu bot automatically',
+ feishu_scan_loading: 'Generating QR code...',
+ feishu_scan_waiting: 'Scan with Feishu to authorize',
+ feishu_scan_tip: 'Confirm the authorization in Feishu after scanning',
+ feishu_scan_open_link: 'Open authorization link',
+ feishu_scan_success: 'Registered',
+ feishu_scan_expired: 'QR code expired',
+ feishu_scan_denied: 'Authorization denied',
+ feishu_scan_fail: 'Registration failed',
+ feishu_scan_retry: 'Retry',
+ tasks_title: 'Scheduled Tasks',
+ tasks_desc: 'View and manage scheduled tasks',
+ tasks_active: 'Active',
+ tasks_paused: 'Paused',
+ tasks_empty: 'No scheduled tasks',
+ tasks_empty_guide: 'Tell the agent "remind me every day at 9am…" in chat to create a scheduled task',
+ tasks_go_chat: 'Create in chat',
+ tasks_next_run: 'Next run',
+ tasks_loading: 'Loading scheduled tasks...',
+ task_edit_title: 'Edit Task',
+ task_name: 'Name',
+ task_enabled: 'Enabled',
+ task_schedule_type: 'Schedule type',
+ task_type_cron: 'Cron',
+ task_type_interval: 'Interval',
+ task_type_once: 'Once',
+ task_cron_expr: 'Cron expression',
+ task_cron_hint: 'e.g. 0 9 * * * runs daily at 9am',
+ task_interval_seconds: 'Interval (seconds)',
+ task_once_time: 'Run at',
+ task_action_type: 'Action type',
+ task_action_send: 'Send message',
+ task_action_agent: 'Agent task',
+ task_message_content: 'Message content',
+ task_task_description: 'Task description',
+ task_channel: 'Channel',
+ task_receiver: 'Receiver',
+ task_channel_locked: 'Channel and receiver cannot be changed after creation',
+ task_save: 'Save',
+ task_cancel: 'Cancel',
+ task_delete: 'Delete',
+ task_delete_confirm: 'Delete this task? This cannot be undone.',
+ task_save_error: 'Failed to save',
+ logs_title: 'Logs',
+ logs_desc: 'Real-time log output (run.log)',
+ logs_live: 'Live',
+ logs_connecting: 'Log streaming will connect shortly...',
+ status_starting: 'Starting CowAgent...',
+ status_starting_desc: 'Initializing the client, please wait',
+ status_error: 'Initialization Failed',
+ status_error_desc: 'Failed to initialize the client, please retry',
+ status_retry: 'Retry',
+ },
+}
+
+export type Lang = 'zh' | 'en'
+
+// First-run default: follow the OS language so zh-* systems start in Chinese
+// and everyone else in English. Once the user picks a language it's persisted
+// in cow_lang and always wins. Falls back to 'en' when the locale is unknown.
+function detectDefaultLang(): Lang {
+ const locale = (window.electronAPI?.systemLocale || navigator.language || '').toLowerCase()
+ return locale.startsWith('zh') ? 'zh' : 'en'
+}
+
+const savedLang = localStorage.getItem('cow_lang') as Lang | null
+let currentLang: Lang = savedLang === 'zh' || savedLang === 'en' ? savedLang : detectDefaultLang()
+
+export function t(key: string): string {
+ return translations[currentLang]?.[key] || translations['en']?.[key] || key
+}
+
+export function getLang(): Lang {
+ return currentLang
+}
+
+export function setLang(lang: Lang) {
+ currentLang = lang
+ localStorage.setItem('cow_lang', lang)
+}
+
+/** Resolve a possibly-localized label ({zh,en} or plain string) for the current language. */
+export function localizedLabel(label: string | { zh: string; en: string } | undefined): string {
+ if (!label) return ''
+ if (typeof label === 'string') return label
+ return label[currentLang] || label.en || label.zh || ''
+}
diff --git a/desktop/src/renderer/src/index.css b/desktop/src/renderer/src/index.css
new file mode 100644
index 00000000..ef115f03
--- /dev/null
+++ b/desktop/src/renderer/src/index.css
@@ -0,0 +1,338 @@
+@import './highlight.css';
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* ============================================================
+ Design tokens — semantic CSS variables driving both themes.
+ Components reference these via Tailwind semantic classes
+ (bg-surface, text-primary, border-default, etc.), so theme
+ switching only swaps variable values, never component code.
+ ============================================================ */
+
+:root {
+ /* Brand accent (CowAgent green), used sparingly */
+ --accent: #4abe6e;
+ --accent-hover: #35a85b;
+ --accent-active: #228547;
+ --accent-soft: rgba(74, 190, 110, 0.12);
+ --accent-contrast: #ffffff;
+
+ /* Status colors */
+ --success: #4abe6e;
+ --warning: #f59e0b;
+ --danger: #ef4444;
+ --danger-soft: rgba(239, 68, 68, 0.1);
+ --danger-border: rgba(239, 68, 68, 0.3);
+ --info: #3b82f6;
+
+ /* Light theme — layered neutral surfaces */
+ --bg-base: #fafafa; /* app background */
+ --bg-surface: #ffffff; /* panels, cards */
+ --bg-surface-2: #f4f4f5; /* nested surfaces, hover fills */
+ --bg-elevated: #ffffff; /* popovers, menus, modals */
+ --bg-inset: #f4f4f5; /* inputs, code blocks */
+
+ --text-primary: #18181b; /* headings, primary text (contrast > 4.5:1) */
+ --text-secondary: #52525b; /* body, labels */
+ --text-tertiary: #71717a; /* hints, captions */
+ --text-disabled: #a1a1aa;
+
+ --border-default: #e4e4e7;
+ --border-strong: #d4d4d8;
+ --border-subtle: #f0f0f1;
+
+ --overlay: rgba(0, 0, 0, 0.4);
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.06);
+ --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.06), 0 4px 16px rgba(0, 0, 0, 0.08);
+ --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12);
+
+ /* Chat-specific tokens (AI-Native UI) */
+ --user-bubble-bg: var(--accent-soft);
+ --ai-bubble-bg: transparent;
+ --message-gap: 16px;
+
+ color-scheme: light;
+}
+
+.dark {
+ /* Dark theme — layered greys instead of harsh pure black */
+ --bg-base: #0e0e10;
+ --bg-surface: #161618;
+ --bg-surface-2: #1c1c1f;
+ --bg-elevated: #1f1f23;
+ --bg-inset: #161618;
+
+ --text-primary: #f4f4f5; /* contrast > 7:1 on bg-base */
+ --text-secondary: #c4c4cc;
+ --text-tertiary: #8e8e96;
+ --text-disabled: #5a5a62;
+
+ --border-default: rgba(255, 255, 255, 0.08);
+ --border-strong: rgba(255, 255, 255, 0.14);
+ --border-subtle: rgba(255, 255, 255, 0.04);
+
+ --accent-contrast: #0e0e10;
+
+ --overlay: rgba(0, 0, 0, 0.6);
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.3);
+ --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.5);
+
+ --user-bubble-bg: rgba(74, 190, 110, 0.16);
+
+ color-scheme: dark;
+}
+
+/* ============================================================
+ Base
+ ============================================================ */
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ height: 100%;
+}
+
+body {
+ background: var(--bg-base);
+ color: var(--text-primary);
+ font-family: 'Inter', system-ui, -apple-system, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
+ font-size: 14px;
+ line-height: 1.6;
+ overflow: hidden;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+}
+
+/* Smooth theme transition (respect reduced motion) */
+body,
+body * {
+ transition-property: background-color, border-color, color, fill, stroke;
+ transition-duration: 200ms;
+ transition-timing-function: ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ transition-duration: 0.01ms !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ }
+}
+
+/* ============================================================
+ Scrollbar — thin, low-key, theme-aware
+ ============================================================ */
+
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--border-strong) transparent;
+}
+
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border-strong);
+ border-radius: 4px;
+ border: 2px solid transparent;
+ background-clip: padding-box;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--text-tertiary);
+ background-clip: padding-box;
+}
+
+/* Windows scrollbars are slightly wider/more visible */
+.platform-win ::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+}
+
+/* ============================================================
+ Titlebar drag regions (frameless window)
+ ============================================================ */
+
+.titlebar-drag {
+ -webkit-app-region: drag;
+}
+
+.titlebar-no-drag {
+ -webkit-app-region: no-drag;
+}
+
+/* ============================================================
+ Animations
+ ============================================================ */
+
+@keyframes blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+
+.animate-blink {
+ animation: blink 1s step-end infinite;
+}
+
+/* AI typing indicator — 3-dot pulse */
+@keyframes typingPulse {
+ 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
+ 40% { transform: scale(1); opacity: 1; }
+}
+
+.typing-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 9999px;
+ background: var(--text-tertiary);
+ animation: typingPulse 1.4s infinite ease-in-out both;
+}
+
+.typing-dot:nth-child(2) { animation-delay: 0.16s; }
+.typing-dot:nth-child(3) { animation-delay: 0.32s; }
+
+/* Smooth content reveal */
+@keyframes fadeInUp {
+ from { opacity: 0; transform: translateY(4px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.animate-reveal {
+ animation: fadeInUp 0.25s ease both;
+}
+
+/* Skeleton shimmer */
+@keyframes shimmer {
+ 0% { background-position: -200% 0; }
+ 100% { background-position: 200% 0; }
+}
+
+.skeleton {
+ background: linear-gradient(
+ 90deg,
+ var(--bg-surface-2) 25%,
+ var(--border-subtle) 50%,
+ var(--bg-surface-2) 75%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ border-radius: 6px;
+}
+
+/* ============================================================
+ Markdown content (assistant messages, memory/knowledge viewers)
+ ============================================================ */
+
+.msg-content > *:first-child { margin-top: 0; }
+.msg-content > *:last-child { margin-bottom: 0; }
+.msg-content p { margin: 0.5em 0; line-height: 1.7; }
+.msg-content ul, .msg-content ol { padding-left: 1.4em; margin: 0.5em 0; }
+.msg-content li { margin: 0.25em 0; }
+.msg-content h1, .msg-content h2, .msg-content h3, .msg-content h4 {
+ font-weight: 600;
+ margin: 0.8em 0 0.4em;
+ color: var(--text-primary);
+}
+.msg-content h1 { font-size: 1.4em; }
+.msg-content h2 { font-size: 1.25em; }
+.msg-content h3 { font-size: 1.1em; }
+
+.msg-content a { color: var(--accent); text-decoration: none; }
+.msg-content a:hover { text-decoration: underline; }
+
+.msg-content blockquote {
+ padding-left: 0.9em;
+ margin: 0.6em 0;
+ border-left: 3px solid var(--accent);
+ color: var(--text-secondary);
+}
+
+.msg-content table {
+ border-collapse: collapse;
+ width: 100%;
+ margin: 0.8em 0;
+ font-size: 0.9em;
+}
+.msg-content th, .msg-content td {
+ border: 1px solid var(--border-default);
+ padding: 0.4em 0.7em;
+ text-align: left;
+}
+.msg-content th { background: var(--bg-surface-2); font-weight: 600; }
+
+.msg-content hr { border: none; border-top: 1px solid var(--border-default); margin: 1em 0; }
+
+/* Inline code */
+.msg-content :not(pre) > code {
+ padding: 0.12em 0.4em;
+ border-radius: 5px;
+ background: var(--bg-inset);
+ border: 1px solid var(--border-subtle);
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
+ font-size: 0.85em;
+}
+
+/* Fenced code blocks */
+.msg-content .code-block-wrapper {
+ margin: 0.7em 0;
+ border-radius: 10px;
+ overflow: hidden;
+ border: 1px solid var(--border-default);
+ background: var(--bg-inset);
+}
+.msg-content .code-block-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 5px 10px 5px 12px;
+ background: var(--bg-surface-2);
+ border-bottom: 1px solid var(--border-subtle);
+}
+.msg-content .code-block-lang {
+ font-size: 11px;
+ font-weight: 500;
+ color: var(--text-tertiary);
+ text-transform: lowercase;
+ letter-spacing: 0.02em;
+}
+.msg-content .code-copy-btn {
+ font-size: 11px;
+ color: var(--text-tertiary);
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 2px 6px;
+ border-radius: 5px;
+ transition: color 0.15s, background 0.15s;
+}
+.msg-content .code-copy-btn:hover { color: var(--text-secondary); background: var(--bg-inset); }
+.msg-content .code-copy-btn.copied { color: var(--accent); }
+.msg-content .code-block-wrapper pre {
+ margin: 0;
+ padding: 12px 14px;
+ overflow-x: auto;
+ background: transparent;
+}
+.msg-content .code-block-wrapper pre code,
+.msg-content pre code.hljs {
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
+ font-size: 12.5px;
+ line-height: 1.6;
+ background: transparent;
+ padding: 0;
+}
diff --git a/desktop/src/renderer/src/layout/NavRail.tsx b/desktop/src/renderer/src/layout/NavRail.tsx
new file mode 100644
index 00000000..189a227d
--- /dev/null
+++ b/desktop/src/renderer/src/layout/NavRail.tsx
@@ -0,0 +1,142 @@
+import React from 'react'
+import { useLocation, useNavigate } from 'react-router-dom'
+import {
+ MessageSquare,
+ BookOpen,
+ Brain,
+ Zap,
+ Radio,
+ Clock,
+ Settings,
+ PanelLeftClose,
+ PanelLeftOpen,
+ Sun,
+ Moon,
+ ScrollText,
+} from 'lucide-react'
+import type { LucideIcon } from 'lucide-react'
+import { t, getLang, setLang, Lang } from '../i18n'
+import { useUIStore } from '../store/uiStore'
+import { useTheme } from '../hooks/useTheme'
+import UpdateBanner from '../components/UpdateBanner'
+
+interface NavItem {
+ path: string
+ labelKey: string
+ icon: LucideIcon
+}
+
+const NAV_ITEMS: NavItem[] = [
+ { path: '/', labelKey: 'menu_chat', icon: MessageSquare },
+ { path: '/knowledge', labelKey: 'menu_knowledge', icon: BookOpen },
+ { path: '/memory', labelKey: 'menu_memory', icon: Brain },
+ { path: '/skills', labelKey: 'menu_skills', icon: Zap },
+ { path: '/channels', labelKey: 'menu_channels', icon: Radio },
+ { path: '/tasks', labelKey: 'menu_tasks', icon: Clock },
+ { path: '/settings', labelKey: 'menu_settings', icon: Settings },
+]
+
+interface NavRailProps {
+ onLangChange: () => void
+}
+
+const NavRail: React.FC = ({ onLangChange }) => {
+ const location = useLocation()
+ const navigate = useNavigate()
+ const { navCollapsed, toggleNav } = useUIStore()
+ const { theme, toggleTheme } = useTheme()
+
+ const collapsed = navCollapsed
+ const width = collapsed ? 'w-[56px]' : 'w-[208px]'
+
+ const toggleLanguage = () => {
+ const next: Lang = getLang() === 'zh' ? 'en' : 'zh'
+ setLang(next)
+ onLangChange()
+ }
+
+ return (
+
+ )
+}
+
+const FooterBtn: React.FC<{
+ collapsed: boolean
+ onClick: () => void
+ title: string
+ active?: boolean
+ children: React.ReactNode
+}> = ({ collapsed, onClick, title, active, children }) => (
+
+ {children}
+
+)
+
+export default NavRail
diff --git a/desktop/src/renderer/src/layout/SessionList.tsx b/desktop/src/renderer/src/layout/SessionList.tsx
new file mode 100644
index 00000000..b969832d
--- /dev/null
+++ b/desktop/src/renderer/src/layout/SessionList.tsx
@@ -0,0 +1,180 @@
+import React, { useEffect, useMemo, useState } from 'react'
+import { Plus, MessageSquare, Pencil, Trash2, Check, X, PanelLeftClose } from 'lucide-react'
+import { t } from '../i18n'
+import { useSessionStore } from '../store/sessionStore'
+import { useUIStore } from '../store/uiStore'
+import type { SessionItem } from '../types'
+
+function groupByTime(sessions: SessionItem[]): { label: string; items: SessionItem[] }[] {
+ const now = new Date()
+ const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime() / 1000
+ const startOfYesterday = startOfToday - 86400
+
+ const today: SessionItem[] = []
+ const yesterday: SessionItem[] = []
+ const earlier: SessionItem[] = []
+
+ for (const s of sessions) {
+ const ts = s.last_active || s.created_at
+ if (ts >= startOfToday) today.push(s)
+ else if (ts >= startOfYesterday) yesterday.push(s)
+ else earlier.push(s)
+ }
+
+ return [
+ { label: t('session_today'), items: today },
+ { label: t('session_yesterday'), items: yesterday },
+ { label: t('session_earlier'), items: earlier },
+ ].filter((g) => g.items.length > 0)
+}
+
+const SessionList: React.FC = () => {
+ const { sessions, activeId, loading, loadSessions, loadMore, hasMore, setActive, newSession, rename, remove } =
+ useSessionStore()
+ const toggleSessions = useUIStore((s) => s.toggleSessions)
+ const [editingId, setEditingId] = useState(null)
+ const [editValue, setEditValue] = useState('')
+
+ useEffect(() => {
+ loadSessions(1)
+ }, [loadSessions])
+
+ const groups = useMemo(() => groupByTime(sessions), [sessions])
+
+ const startEdit = (s: SessionItem) => {
+ setEditingId(s.session_id)
+ setEditValue(s.title || '')
+ }
+
+ const commitEdit = async () => {
+ if (editingId && editValue.trim()) {
+ await rename(editingId, editValue.trim())
+ }
+ setEditingId(null)
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
newSession()}
+ title={t('session_new')}
+ className="titlebar-no-drag inline-flex items-center gap-1.5 px-2.5 h-7 rounded-btn text-[12px] font-medium text-accent hover:bg-accent-soft cursor-pointer transition-colors"
+ >
+
+ {t('session_new')}
+
+
+
+ {/* List */}
+
{
+ const el = e.currentTarget
+ if (el.scrollHeight - el.scrollTop - el.clientHeight < 80 && hasMore && !loading) loadMore()
+ }}
+ >
+ {sessions.length === 0 && !loading && (
+
+
+
{t('session_empty')}
+
+ )}
+
+ {groups.map((group) => (
+
+
+ {group.label}
+
+ {group.items.map((s) => {
+ const isActive = s.session_id === activeId
+ const isEditing = editingId === s.session_id
+ return (
+
!isEditing && setActive(s.session_id)}
+ className={`group flex items-center gap-2 px-2 h-9 rounded-btn cursor-pointer transition-colors ${
+ isActive ? 'bg-accent-soft' : 'hover:bg-surface-2'
+ }`}
+ >
+ {isEditing ? (
+
setEditValue(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') commitEdit()
+ if (e.key === 'Escape') setEditingId(null)
+ }}
+ onClick={(e) => e.stopPropagation()}
+ className="flex-1 min-w-0 bg-inset border border-strong rounded px-1.5 py-0.5 text-[13px] text-content focus:outline-none focus:border-accent"
+ />
+ ) : (
+
+ {s.title || s.session_id}
+
+ )}
+
+ {isEditing ? (
+
+ { e.stopPropagation(); commitEdit() }}>
+ { e.stopPropagation(); setEditingId(null) }}>
+
+ ) : (
+
+
{ e.stopPropagation(); startEdit(s) }} title={t('session_rename')}>
+
+
+
{ e.stopPropagation(); remove(s.session_id) }} title={t('session_delete')} danger>
+
+
+
+ )}
+
+ )
+ })}
+
+ ))}
+
+ {loading && (
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ )}
+
+
+ )
+}
+
+const IconBtn: React.FC<{
+ onClick: (e: React.MouseEvent) => void
+ title?: string
+ danger?: boolean
+ children: React.ReactNode
+}> = ({ onClick, title, danger, children }) => (
+
+ {children}
+
+)
+
+export default SessionList
diff --git a/desktop/src/renderer/src/layout/WindowControls.tsx b/desktop/src/renderer/src/layout/WindowControls.tsx
new file mode 100644
index 00000000..511aa6f6
--- /dev/null
+++ b/desktop/src/renderer/src/layout/WindowControls.tsx
@@ -0,0 +1,42 @@
+import React, { useEffect, useState } from 'react'
+import { Minus, Square, Copy, X } from 'lucide-react'
+
+/**
+ * Custom window controls for the frameless Windows titlebar.
+ * On macOS the system renders traffic lights, so this returns null there.
+ */
+const WindowControls: React.FC = () => {
+ const [maximized, setMaximized] = useState(false)
+ const api = window.electronAPI
+
+ useEffect(() => {
+ api?.windowIsMaximized().then(setMaximized)
+ const off = api?.onMaximizeChange(setMaximized)
+ return off
+ }, [api])
+
+ if (api?.platform === 'darwin') return null
+
+ const btn =
+ 'titlebar-no-drag inline-flex items-center justify-center w-11 h-full text-content-tertiary hover:text-content cursor-pointer transition-colors'
+
+ return (
+
+ api?.windowMinimize()} aria-label="Minimize">
+
+
+ api?.windowMaximize()} aria-label="Maximize">
+ {maximized ? : }
+
+ api?.windowClose()}
+ aria-label="Close"
+ >
+
+
+
+ )
+}
+
+export default WindowControls
diff --git a/desktop/src/renderer/src/main.tsx b/desktop/src/renderer/src/main.tsx
new file mode 100644
index 00000000..158640ab
--- /dev/null
+++ b/desktop/src/renderer/src/main.tsx
@@ -0,0 +1,13 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { HashRouter } from 'react-router-dom'
+import App from './App'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+)
diff --git a/desktop/src/renderer/src/pages/ChannelsPage.tsx b/desktop/src/renderer/src/pages/ChannelsPage.tsx
new file mode 100644
index 00000000..78e86f91
--- /dev/null
+++ b/desktop/src/renderer/src/pages/ChannelsPage.tsx
@@ -0,0 +1,271 @@
+import React, { useEffect, useMemo, useState } from 'react'
+import { Loader2, Plug, QrCode } from 'lucide-react'
+import { t, localizedLabel } from '../i18n'
+import apiClient from '../api/client'
+import type { ChannelInfo, ChannelField } from '../types'
+import { Toggle, Btn } from './settings/primitives'
+import QrLoginModal from '../components/QrLoginModal'
+
+// Channels that connect via QR scanning rather than credential fields.
+const QR_PROVIDERS: Record = { weixin: 'weixin', feishu: 'feishu' }
+
+interface ChannelsPageProps {
+ baseUrl: string
+}
+
+// A masked secret looks like "abcd****wxyz"; the backend skips such values.
+const MASK_RE = /\*{2,}/
+
+const ChannelsPage: React.FC = ({ baseUrl }) => {
+ const [channels, setChannels] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ const loadChannels = async () => {
+ try {
+ setLoading(true)
+ const data = await apiClient.getChannels()
+ setChannels(data || [])
+ } catch (err) {
+ console.error('Failed to load channels:', err)
+ setChannels([])
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ apiClient.setBaseUrl(baseUrl)
+ void loadChannels()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [baseUrl])
+
+ const { connected, available } = useMemo(() => {
+ const connected = channels.filter((c) => c.active)
+ const available = channels.filter((c) => !c.active)
+ return { connected, available }
+ }, [channels])
+
+ return (
+
+
+
{t('channels_title')}
+
{t('channels_desc')}
+
+
+
+
+ {loading ? (
+
+
+ {t('channels_loading')}
+
+ ) : (
+
+
+ {connected.length === 0 ? (
+ {t('channels_empty_connected')}
+ ) : (
+ connected.map((ch) => )
+ )}
+
+
+ {available.length > 0 && (
+
+ {available.map((ch) => (
+
+ ))}
+
+ )}
+
+ )}
+
+
+
+ )
+}
+
+const Section: React.FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => (
+
+)
+
+const ChannelCard: React.FC<{ channel: ChannelInfo; onChanged: () => void }> = ({ channel, onChanged }) => {
+ // Channels with no fields connect purely via QR (e.g. weixin).
+ const isQrLogin = channel.fields.length === 0
+ // QR provider supported by the desktop scan modal (weixin / feishu).
+ const qrProvider = QR_PROVIDERS[channel.name]
+ const [showQr, setShowQr] = useState(false)
+ const [expanded, setExpanded] = useState(false)
+ const [values, setValues] = useState>(() =>
+ Object.fromEntries(channel.fields.map((f) => [f.key, f.value != null ? String(f.value) : '']))
+ )
+ // Track which secret fields still hold the server-provided mask.
+ const [masked, setMasked] = useState>(() =>
+ Object.fromEntries(
+ channel.fields.map((f) => [f.key, f.type === 'secret' && !!f.value && MASK_RE.test(String(f.value))])
+ )
+ )
+ const [busy, setBusy] = useState(false)
+ const [status, setStatus] = useState('')
+
+ const setField = (key: string, val: string) => setValues((p) => ({ ...p, [key]: val }))
+
+ // Only send fields the user actually changed; masked secrets are skipped so
+ // the backend keeps the stored value (mirrors the web console behavior).
+ const buildConfig = (): Record => {
+ const cfg: Record = {}
+ channel.fields.forEach((f) => {
+ const v = values[f.key]
+ if (f.type === 'secret' && masked[f.key]) return
+ if (v === '' || v == null) return
+ cfg[f.key] = f.type === 'number' ? Number(v) : v
+ })
+ return cfg
+ }
+
+ const run = async (action: 'save' | 'connect' | 'disconnect') => {
+ setBusy(true)
+ setStatus('')
+ try {
+ const cfg = action === 'disconnect' ? undefined : buildConfig()
+ const res = await apiClient.channelAction(action, channel.name, cfg)
+ if (res.status === 'success') {
+ if (action === 'save') {
+ setStatus(t('channels_save_ok'))
+ setTimeout(() => setStatus(''), 1600)
+ }
+ onChanged()
+ } else {
+ setStatus((res.message as string) || t(action === 'connect' ? 'channels_connect_error' : 'channels_save_error'))
+ }
+ } catch {
+ setStatus(t(action === 'connect' ? 'channels_connect_error' : 'channels_save_error'))
+ } finally {
+ setBusy(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+ {localizedLabel(channel.label)}
+
+
+
{channel.name}
+
+
+ {channel.active ? (
+
run('disconnect')} disabled={busy}>
+ {t('channels_disconnect')}
+
+ ) : qrProvider ? (
+
setShowQr(true)}>
+ {qrProvider === 'weixin' ? t('channels_scan_login') : t('channels_scan_register')}
+
+ ) : isQrLogin ? null : (
+
setExpanded((v) => !v)}>
+ {t('channels_add')}
+
+ )}
+
+
+ {/* QR-login channels with no desktop support fall back to the web console. */}
+ {isQrLogin && !channel.active && !qrProvider && (
+
{t('channels_qr_hint')}
+ )}
+
+ {/* Field-bearing QR channels (feishu) can also be configured manually. */}
+ {!isQrLogin && qrProvider && !channel.active && !expanded && (
+
setExpanded(true)}
+ className="text-xs text-content-tertiary hover:text-content-secondary mt-3 pl-12 cursor-pointer transition-colors"
+ >
+ {t('channels_add')}
+
+ )}
+
+ {/* Field editor: always for connected channels with fields, on-demand for available ones. */}
+ {!isQrLogin && (channel.active || expanded) && (
+
+ {channel.fields.map((f) => (
+
setField(f.key, v)}
+ onFocusSecret={() => {
+ if (f.type === 'secret' && masked[f.key]) {
+ setField(f.key, '')
+ setMasked((p) => ({ ...p, [f.key]: false }))
+ }
+ }}
+ />
+ ))}
+
+
+ {status || '\u00a0'}
+
+ {channel.active ? (
+ run('save')} disabled={busy}>
+ {t('channels_save')}
+
+ ) : (
+ run('connect')} disabled={busy}>
+ {t('channels_connect')}
+
+ )}
+
+
+ )}
+
+ {showQr && qrProvider && (
+
setShowQr(false)}
+ onConnected={() => {
+ setShowQr(false)
+ onChanged()
+ }}
+ />
+ )}
+
+ )
+}
+
+const FieldRow: React.FC<{
+ field: ChannelField
+ value: string
+ onChange: (v: string) => void
+ onFocusSecret: () => void
+}> = ({ field, value, onChange, onFocusSecret }) => {
+ if (field.type === 'bool') {
+ return (
+
+ {field.label}
+ onChange(v ? 'true' : 'false')} />
+
+ )
+ }
+ return (
+
+ {field.label}
+ onChange(e.target.value)}
+ onFocus={onFocusSecret}
+ className="w-full px-3 py-2 rounded-btn border border-strong bg-inset text-sm text-content placeholder:text-content-tertiary focus:outline-none focus:border-accent font-mono transition-colors"
+ />
+
+ )
+}
+
+export default ChannelsPage
diff --git a/desktop/src/renderer/src/pages/ChatPage.tsx b/desktop/src/renderer/src/pages/ChatPage.tsx
new file mode 100644
index 00000000..136ee5de
--- /dev/null
+++ b/desktop/src/renderer/src/pages/ChatPage.tsx
@@ -0,0 +1,229 @@
+import React, { useEffect, useRef, useCallback, useState } from 'react'
+import { ChevronUp, Loader2 } from 'lucide-react'
+import MessageBubble from '../components/MessageBubble'
+import ChatInput, { type ChatInputHandle } from '../components/ChatInput'
+import { t } from '../i18n'
+import apiClient from '../api/client'
+import type { Attachment, ChatMessage } from '../types'
+import { useChatStore } from '../store/chatStore'
+import { useSessionStore } from '../store/sessionStore'
+
+interface ChatPageProps {
+ baseUrl: string
+}
+
+const SUGGESTIONS = ['example_sys', 'example_task', 'example_code'] as const
+
+const ChatPage: React.FC = ({ baseUrl }) => {
+ const activeId = useSessionStore((s) => s.activeId)
+ const newSession = useSessionStore((s) => s.newSession)
+ const loadSessions = useSessionStore((s) => s.loadSessions)
+
+ const session = useChatStore((s) => s.sessions[activeId])
+ const send = useChatStore((s) => s.send)
+ const cancel = useChatStore((s) => s.cancel)
+ const regenerate = useChatStore((s) => s.regenerate)
+ const editUserMessage = useChatStore((s) => s.editUserMessage)
+ const deleteMessage = useChatStore((s) => s.deleteMessage)
+ const loadHistory = useChatStore((s) => s.loadHistory)
+ const ensureSession = useChatStore((s) => s.ensureSession)
+
+ const messages = session?.messages ?? []
+ const isStreaming = session?.isStreaming ?? false
+
+ const scrollRef = useRef(null)
+ const bottomRef = useRef(null)
+ const inputResetRef = useRef(null)
+ const [loadingMore, setLoadingMore] = useState(false)
+ const titlePendingRef = useRef(false)
+
+ useEffect(() => {
+ apiClient.setBaseUrl(baseUrl)
+ }, [baseUrl])
+
+ // Load history when switching to a session that hasn't been loaded yet.
+ useEffect(() => {
+ ensureSession(activeId)
+ const s = useChatStore.getState().sessions[activeId]
+ if (s && !s.historyLoaded && !s.isStreaming) {
+ loadHistory(activeId, 1)
+ }
+ }, [activeId, ensureSession, loadHistory])
+
+ const scrollToBottom = useCallback((smooth = true) => {
+ const el = scrollRef.current
+ if (!el) return
+ // Jump straight to the bottom; instant for session switches, smooth for streaming.
+ if (smooth) {
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
+ } else {
+ el.scrollTop = el.scrollHeight
+ }
+ }, [])
+
+ // Snap to the bottom instantly when switching sessions (no top-to-bottom animation).
+ // History may load a frame later, so keep snapping instantly until content arrives.
+ const lastSessionRef = useRef('')
+ const lastLenRef = useRef(0)
+ const pendingSnapRef = useRef(false)
+ useEffect(() => {
+ const el = scrollRef.current
+ if (!el) return
+
+ if (lastSessionRef.current !== activeId) {
+ lastSessionRef.current = activeId
+ lastLenRef.current = messages.length
+ pendingSnapRef.current = true
+ }
+
+ if (pendingSnapRef.current) {
+ // Instant snap on switch and on the first content that lands afterwards.
+ lastLenRef.current = messages.length
+ requestAnimationFrame(() => scrollToBottom(false))
+ if (messages.length > 0) pendingSnapRef.current = false
+ return
+ }
+
+ const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 160
+ const grew = messages.length !== lastLenRef.current
+ lastLenRef.current = messages.length
+ if (nearBottom || grew) scrollToBottom(true)
+ }, [messages, activeId, scrollToBottom])
+
+ const handleSend = useCallback(
+ async (text: string, attachments: Attachment[]) => {
+ const sid = activeId
+ const isFirst = (useChatStore.getState().sessions[sid]?.messages.length ?? 0) === 0
+ titlePendingRef.current = isFirst
+ await send(sid, text, attachments)
+ // After the first message, refresh the list and ask backend to title it.
+ if (isFirst) {
+ try {
+ await apiClient.generateSessionTitle(sid, text)
+ } catch {
+ /* ignore */
+ }
+ loadSessions(1)
+ titlePendingRef.current = false
+ }
+ },
+ [activeId, send, loadSessions]
+ )
+
+ const handleNewChat = useCallback(() => {
+ const id = newSession()
+ ensureSession(id)
+ loadHistory(id, 1)
+ }, [newSession, ensureSession, loadHistory])
+
+ const handleClearContext = useCallback(async () => {
+ try {
+ await apiClient.clearContext(activeId)
+ } catch {
+ /* ignore */
+ }
+ }, [activeId])
+
+ const handleStop = useCallback(() => cancel(activeId), [cancel, activeId])
+
+ const handleRegenerate = useCallback((id: string) => regenerate(activeId, id), [regenerate, activeId])
+
+ const handleEdit = useCallback(
+ (id: string) => {
+ const result = editUserMessage(activeId, id)
+ if (result && inputResetRef.current) inputResetRef.current(result.text, result.attachments)
+ },
+ [editUserMessage, activeId]
+ )
+
+ const handleDelete = useCallback(
+ (msg: ChatMessage) => {
+ if (msg.userSeq != null) deleteMessage(activeId, msg.userSeq, true)
+ },
+ [deleteMessage, activeId]
+ )
+
+ const handleScroll = useCallback(
+ async (e: React.UIEvent) => {
+ const el = e.currentTarget
+ const s = useChatStore.getState().sessions[activeId]
+ if (el.scrollTop < 40 && s?.historyHasMore && !loadingMore && !isStreaming) {
+ setLoadingMore(true)
+ const prevHeight = el.scrollHeight
+ await loadHistory(activeId, s.historyPage + 1)
+ requestAnimationFrame(() => {
+ // preserve scroll position after prepending older messages
+ el.scrollTop = el.scrollHeight - prevHeight
+ setLoadingMore(false)
+ })
+ }
+ },
+ [activeId, loadHistory, loadingMore, isStreaming]
+ )
+
+ const isEmpty = messages.length === 0
+
+ return (
+
+
+ {loadingMore && (
+
+
+
+ )}
+
+ {isEmpty ? (
+
+
+
{t('chat_welcome')}
+
{t('chat_empty_hint')}
+
+
+ {SUGGESTIONS.map((key) => (
+
handleSend(t(`${key}_text` as Parameters[0]), [])}
+ className="text-left bg-surface border border-default rounded-xl p-3.5 cursor-pointer hover:border-accent hover:shadow-sm transition-all"
+ >
+
+ {t(`${key}_title` as Parameters[0])}
+
+
+ {t(`${key}_text` as Parameters[0])}
+
+
+ ))}
+
+
+ ) : (
+
+ {messages.map((msg) => (
+
+ ))}
+
+
+ )}
+
+
+ {/* Jump-to-bottom affordance could go here in a later pass */}
+
+
+
+ )
+}
+
+export default ChatPage
diff --git a/desktop/src/renderer/src/pages/KnowledgePage.tsx b/desktop/src/renderer/src/pages/KnowledgePage.tsx
new file mode 100644
index 00000000..7c91c0eb
--- /dev/null
+++ b/desktop/src/renderer/src/pages/KnowledgePage.tsx
@@ -0,0 +1,385 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
+import {
+ Loader2,
+ Search,
+ FileText,
+ ChevronRight,
+ ChevronDown,
+ MessageSquarePlus,
+ Network,
+ Files,
+} from 'lucide-react'
+import type { LucideIcon } from 'lucide-react'
+import { useNavigate } from 'react-router-dom'
+import { t } from '../i18n'
+import apiClient from '../api/client'
+import type { KnowledgeDir, KnowledgeFile, KnowledgeList, KnowledgeGraph as KnowledgeGraphData } from '../types'
+import Markdown from '../components/Markdown'
+import KnowledgeGraph from '../components/KnowledgeGraph'
+
+interface KnowledgePageProps {
+ baseUrl: string
+}
+
+type Tab = 'docs' | 'graph'
+
+const formatSize = (bytes: number): string => {
+ if (bytes < 1024) return bytes + ' B'
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
+}
+
+// Find the first document (root files first, then a DFS over the tree).
+function firstFile(list: KnowledgeList): { path: string; title: string } | null {
+ const root = list.root_files?.[0]
+ if (root) return { path: root.name, title: root.title || root.name }
+ const walk = (dir: KnowledgeDir, prefix: string): { path: string; title: string } | null => {
+ const dirPath = prefix ? `${prefix}/${dir.dir}` : dir.dir
+ const f = dir.files[0]
+ if (f) return { path: `${dirPath}/${f.name}`, title: f.title || f.name }
+ for (const c of dir.children) {
+ const hit = walk(c, dirPath)
+ if (hit) return hit
+ }
+ return null
+ }
+ for (const d of list.tree || []) {
+ const hit = walk(d, '')
+ if (hit) return hit
+ }
+ return null
+}
+
+const KnowledgePage: React.FC = ({ baseUrl }) => {
+ const navigate = useNavigate()
+ const [tab, setTab] = useState('docs')
+ const [data, setData] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [search, setSearch] = useState('')
+
+ const [activePath, setActivePath] = useState(null)
+ const [docTitle, setDocTitle] = useState('')
+ const [content, setContent] = useState('')
+ const [docLoading, setDocLoading] = useState(false)
+
+ const [graph, setGraph] = useState(null)
+ const [graphLoading, setGraphLoading] = useState(false)
+
+ const openDoc = useCallback(async (path: string, title: string) => {
+ setActivePath(path)
+ setDocTitle(title)
+ setDocLoading(true)
+ setContent('')
+ try {
+ const res = await apiClient.readKnowledge(path)
+ setContent(res.content || '')
+ } catch {
+ setContent(`> ${t('knowledge_doc_load_error')}`)
+ } finally {
+ setDocLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ apiClient.setBaseUrl(baseUrl)
+ let cancelled = false
+ ;(async () => {
+ try {
+ setLoading(true)
+ const fresh = await apiClient.getKnowledgeList()
+ if (cancelled) return
+ setData(fresh)
+ // Auto-open the first document so the viewer isn't empty on entry.
+ const first = firstFile(fresh)
+ if (first) void openDoc(first.path, first.title)
+ } catch (e) {
+ console.error('Failed to load knowledge:', e)
+ } finally {
+ if (!cancelled) setLoading(false)
+ }
+ })()
+ return () => {
+ cancelled = true
+ }
+ }, [baseUrl, openDoc])
+
+ const loadGraph = useCallback(async () => {
+ if (graph) return
+ setGraphLoading(true)
+ try {
+ setGraph(await apiClient.getKnowledgeGraph())
+ } catch (e) {
+ console.error('Failed to load graph:', e)
+ setGraph({ nodes: [], links: [] })
+ } finally {
+ setGraphLoading(false)
+ }
+ }, [graph])
+
+ const switchTab = (next: Tab) => {
+ setTab(next)
+ if (next === 'graph') void loadGraph()
+ }
+
+ // Jump from a graph node to its document.
+ const onGraphSelect = useCallback(
+ (id: string, label: string) => {
+ setTab('docs')
+ void openDoc(id, label)
+ },
+ [openDoc]
+ )
+
+ const totalPages = data?.stats?.pages ?? 0
+ const statsLabel = useMemo(() => {
+ if (!data) return ''
+ return t('knowledge_stats')
+ .replace('{pages}', String(data.stats?.pages ?? 0))
+ .replace('{size}', formatSize(data.stats?.size ?? 0))
+ }, [data])
+
+ if (loading) {
+ return (
+
+
+ {t('knowledge_loading')}
+
+ )
+ }
+
+ const isEmpty = !data || totalPages === 0
+
+ if (isEmpty) {
+ return (
+
+
+
+
+
+ {data?.enabled === false ? t('knowledge_disabled') : t('knowledge_empty')}
+
+
{t('knowledge_empty_guide')}
+
navigate('/')}
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-btn bg-accent text-accent-contrast hover:bg-accent-hover text-sm font-medium cursor-pointer transition-colors"
+ >
+
+ {t('knowledge_go_chat')}
+
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
{t('knowledge_title')}
+
{statsLabel}
+
+
+ switchTab('docs')} />
+ switchTab('graph')}
+ />
+
+
+
+ {tab === 'docs' ? (
+
+ {/* Tree sidebar */}
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder={t('knowledge_search')}
+ className="w-full pl-8 pr-3 py-2 rounded-btn border border-strong bg-inset text-sm text-content placeholder:text-content-tertiary focus:outline-none focus:border-accent transition-colors"
+ />
+
+
+
+
+
+
+
+ {/* Document viewer */}
+
+ {!activePath ? (
+
+
+
{t('knowledge_select_hint')}
+
+ ) : (
+
+
{docTitle}
+
{activePath}
+ {docLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ ) : (
+
+ {graphLoading ? (
+
+
+
+ ) : graph && graph.nodes.length > 0 ? (
+
+ ) : (
+
+ {t('knowledge_graph_empty')}
+
+ )}
+
+ )}
+
+ )
+}
+
+const TabBtn: React.FC<{
+ icon: LucideIcon
+ label: string
+ active: boolean
+ onClick: () => void
+}> = ({ icon: Icon, label, active, onClick }) => (
+
+
+ {label}
+
+)
+
+// ---- Tree rendering --------------------------------------------------------
+
+const Tree: React.FC<{
+ data: KnowledgeList
+ search: string
+ activePath: string | null
+ onOpen: (path: string, title: string) => void
+}> = ({ data, search, activePath, onOpen }) => {
+ const matches = (f: KnowledgeFile) => !search || f.title.toLowerCase().includes(search) || f.name.toLowerCase().includes(search)
+
+ return (
+
+ {(data.root_files || []).filter(matches).map((f) => (
+
+ ))}
+ {(data.tree || []).map((dir) => (
+
+ ))}
+
+ )
+}
+
+// Count files in a dir subtree that match the search.
+function countMatches(dir: KnowledgeDir, search: string): number {
+ const own = dir.files.filter(
+ (f) => !search || f.title.toLowerCase().includes(search) || f.name.toLowerCase().includes(search)
+ ).length
+ return own + dir.children.reduce((acc, c) => acc + countMatches(c, search), 0)
+}
+
+const DirNode: React.FC<{
+ dir: KnowledgeDir
+ prefix: string
+ search: string
+ activePath: string | null
+ onOpen: (path: string, title: string) => void
+}> = ({ dir, prefix, search, activePath, onOpen }) => {
+ const dirPath = prefix ? `${prefix}/${dir.dir}` : dir.dir
+ const [open, setOpen] = useState(true)
+ const matchCount = search ? countMatches(dir, search) : dir.files.length + dir.children.length
+ if (search && matchCount === 0) return null
+
+ const visibleFiles = dir.files.filter(
+ (f) => !search || f.title.toLowerCase().includes(search) || f.name.toLowerCase().includes(search)
+ )
+ const expanded = open || !!search
+
+ return (
+
+
setOpen((v) => !v)}
+ className="w-full flex items-center gap-1 px-2 py-1.5 rounded-btn text-sm text-content-secondary hover:bg-surface-2 cursor-pointer transition-colors"
+ >
+ {expanded ? : }
+ {dir.dir}
+ {matchCount}
+
+ {expanded && (
+
+ {visibleFiles.map((f) => {
+ const fpath = `${dirPath}/${f.name}`
+ return (
+
+ )
+ })}
+ {dir.children.map((c) => (
+
+ ))}
+
+ )}
+
+ )
+}
+
+const FileLeaf: React.FC<{
+ path: string
+ title: string
+ active: boolean
+ onOpen: (path: string, title: string) => void
+}> = ({ path, title, active, onOpen }) => (
+ onOpen(path, title)}
+ className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-btn text-sm cursor-pointer transition-colors text-left ${
+ active ? 'bg-accent-soft text-accent' : 'text-content-secondary hover:bg-surface-2'
+ }`}
+ >
+
+ {title}
+
+)
+
+export default KnowledgePage
diff --git a/desktop/src/renderer/src/pages/LogsPage.tsx b/desktop/src/renderer/src/pages/LogsPage.tsx
new file mode 100644
index 00000000..e7669d97
--- /dev/null
+++ b/desktop/src/renderer/src/pages/LogsPage.tsx
@@ -0,0 +1,104 @@
+import React, { useState, useEffect, useRef } from 'react'
+import { t } from '../i18n'
+import apiClient from '../api/client'
+
+interface LogsPageProps {
+ baseUrl: string
+}
+
+const LogsPage: React.FC = ({ baseUrl }) => {
+ const [logs, setLogs] = useState([])
+ const [autoScroll, setAutoScroll] = useState(true)
+ const containerRef = useRef(null)
+
+ useEffect(() => {
+ apiClient.setBaseUrl(baseUrl)
+
+ const es = apiClient.createLogStream()
+
+ es.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data)
+ if (data.type === 'init' && data.content) {
+ setLogs(data.content.split('\n').filter(Boolean))
+ } else if (data.type === 'line' && data.content) {
+ setLogs((prev) => {
+ const next = [...prev, data.content]
+ if (next.length > 2000) return next.slice(-1500)
+ return next
+ })
+ }
+ } catch { /* ignore */ }
+ }
+
+ return () => es.close()
+ }, [baseUrl])
+
+ useEffect(() => {
+ if (autoScroll && containerRef.current) {
+ containerRef.current.scrollTop = containerRef.current.scrollHeight
+ }
+ }, [logs, autoScroll])
+
+ const handleScroll = () => {
+ if (!containerRef.current) return
+ const { scrollTop, scrollHeight, clientHeight } = containerRef.current
+ setAutoScroll(scrollHeight - scrollTop - clientHeight < 50)
+ }
+
+ const getLogColor = (line: string) => {
+ if (line.includes('ERROR') || line.includes('error')) return 'text-red-400'
+ if (line.includes('WARNING') || line.includes('warn')) return 'text-amber-400'
+ if (line.includes('DEBUG')) return 'text-slate-500'
+ return 'text-slate-300'
+ }
+
+ return (
+
+
+
+
+
{t('logs_title')}
+
{t('logs_desc')}
+
+
+
+ {/* Terminal-style log viewer */}
+
+ {/* Terminal header */}
+
+
+
+
+
+
+
run.log
+
+
+
+ {t('logs_live')}
+
+
+
+ {/* Log content */}
+
+ {logs.length > 0 ? (
+ logs.map((line, i) => (
+
{line}
+ ))
+ ) : (
+
{t('logs_connecting')}
+ )}
+
+
+
+
+ )
+}
+
+export default LogsPage
diff --git a/desktop/src/renderer/src/pages/MemoryPage.tsx b/desktop/src/renderer/src/pages/MemoryPage.tsx
new file mode 100644
index 00000000..4ef20335
--- /dev/null
+++ b/desktop/src/renderer/src/pages/MemoryPage.tsx
@@ -0,0 +1,256 @@
+import React, { useCallback, useEffect, useState } from 'react'
+import { Loader2, ArrowLeft, Brain, Sprout, FileText, ChevronLeft, ChevronRight } from 'lucide-react'
+import type { LucideIcon } from 'lucide-react'
+import { t } from '../i18n'
+import apiClient from '../api/client'
+import type { MemoryItem, MemoryCategory } from '../types'
+import Markdown from '../components/Markdown'
+
+interface MemoryPageProps {
+ baseUrl: string
+}
+
+type Tab = 'files' | 'evolution'
+const PAGE_SIZE = 10
+
+const formatSize = (bytes: number): string => {
+ if (bytes < 1024) return bytes + ' B'
+ return (bytes / 1024).toFixed(1) + ' KB'
+}
+
+// Map a file's `type` to its display badge.
+const typeBadge = (type: string): { label: string; cls: string } => {
+ switch (type) {
+ case 'global':
+ return { label: t('memory_type_global'), cls: 'bg-accent-soft text-accent' }
+ case 'evolution':
+ return { label: t('memory_type_evolution'), cls: 'bg-inset text-success' }
+ case 'dream':
+ return { label: t('memory_type_dream'), cls: 'bg-inset text-info' }
+ default:
+ return { label: t('memory_type_daily'), cls: 'bg-inset text-content-secondary' }
+ }
+}
+
+const MemoryPage: React.FC = ({ baseUrl }) => {
+ const [tab, setTab] = useState('files')
+ const [items, setItems] = useState([])
+ const [total, setTotal] = useState(0)
+ const [page, setPage] = useState(1)
+ const [loading, setLoading] = useState(true)
+
+ const [viewing, setViewing] = useState(null)
+ const [content, setContent] = useState('')
+ const [docLoading, setDocLoading] = useState(false)
+
+ const category: MemoryCategory = tab === 'evolution' ? 'evolution' : 'memory'
+
+ const loadList = useCallback(
+ async (cat: MemoryCategory, p: number) => {
+ try {
+ setLoading(true)
+ const data = await apiClient.getMemoryList(p, PAGE_SIZE, cat)
+ setItems(data.list || [])
+ setTotal(data.total || 0)
+ setPage(data.page || p)
+ } catch (err) {
+ console.error('Failed to load memory:', err)
+ setItems([])
+ setTotal(0)
+ } finally {
+ setLoading(false)
+ }
+ },
+ []
+ )
+
+ useEffect(() => {
+ apiClient.setBaseUrl(baseUrl)
+ void loadList(category, 1)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [baseUrl, tab])
+
+ const openFile = async (item: MemoryItem) => {
+ // In the evolution tab a file lives in its own dir (dream vs evolution).
+ const fileCategory: MemoryCategory =
+ item.type === 'dream' || item.type === 'evolution' ? (item.type as MemoryCategory) : category
+ setViewing(item.filename)
+ setDocLoading(true)
+ setContent('')
+ try {
+ const text = await apiClient.getMemoryContent(item.filename, fileCategory)
+ setContent(text)
+ } catch {
+ setContent(`> ${t('memory_doc_load_error')}`)
+ } finally {
+ setDocLoading(false)
+ }
+ }
+
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
+
+ return (
+
+ {/* Header */}
+
+
+
{t('memory_title')}
+
{t('memory_desc')}
+
+ {!viewing && (
+
+ setTab('files')} />
+ setTab('evolution')}
+ />
+
+ )}
+
+
+ {viewing ? (
+ /* File viewer */
+
+
+
setViewing(null)}
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-btn text-sm text-content-secondary hover:bg-inset border border-strong transition-colors cursor-pointer"
+ >
+
+ {t('memory_back')}
+
+
{viewing}
+
+
+
+ {docLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+ ) : (
+ /* List */
+
+
+ {loading ? (
+
+
+ {t('memory_loading')}
+
+ ) : items.length === 0 ? (
+
+ {tab === 'evolution' ?
:
}
+
{tab === 'evolution' ? t('memory_empty_evolution') : t('memory_empty_files')}
+
+ ) : (
+ <>
+
+
+
+
+ {t('memory_col_name')}
+ {t('memory_col_type')}
+ {t('memory_col_size')}
+ {t('memory_col_updated')}
+
+
+
+ {items.map((item) => {
+ const badge = typeBadge(item.type)
+ return (
+ openFile(item)}
+ className="border-b border-subtle last:border-0 hover:bg-inset cursor-pointer transition-colors"
+ >
+
+
+
+ {item.filename}
+
+
+
+ {badge.label}
+
+ {formatSize(item.size)}
+ {item.updated_at}
+
+ )
+ })}
+
+
+
+
+ {totalPages > 1 && (
+
+
+ {page} / {totalPages}
+
+
+
loadList(category, page - 1)} />
+ = totalPages}
+ onClick={() => loadList(category, page + 1)}
+ iconRight
+ />
+
+
+ )}
+ >
+ )}
+
+
+ )}
+
+ )
+}
+
+const Th: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+ {children}
+)
+
+const TabBtn: React.FC<{ icon: LucideIcon; label: string; active: boolean; onClick: () => void }> = ({
+ icon: Icon,
+ label,
+ active,
+ onClick,
+}) => (
+
+
+ {label}
+
+)
+
+const PageBtn: React.FC<{
+ icon: LucideIcon
+ label: string
+ disabled: boolean
+ onClick: () => void
+ iconRight?: boolean
+}> = ({ icon: Icon, label, disabled, onClick, iconRight }) => (
+
+ {!iconRight && }
+ {label}
+ {iconRight && }
+
+)
+
+export default MemoryPage
diff --git a/desktop/src/renderer/src/pages/PlaceholderPage.tsx b/desktop/src/renderer/src/pages/PlaceholderPage.tsx
new file mode 100644
index 00000000..e1d414b4
--- /dev/null
+++ b/desktop/src/renderer/src/pages/PlaceholderPage.tsx
@@ -0,0 +1,20 @@
+import React from 'react'
+import { Construction } from 'lucide-react'
+
+interface PlaceholderPageProps {
+ title: string
+ hint?: string
+}
+
+/** Temporary page for routes that will be implemented in later phases. */
+const PlaceholderPage: React.FC = ({ title, hint }) => (
+
+
+
+
+
{title}
+
{hint || 'Coming soon in this iteration.'}
+
+)
+
+export default PlaceholderPage
diff --git a/desktop/src/renderer/src/pages/SettingsPage.tsx b/desktop/src/renderer/src/pages/SettingsPage.tsx
new file mode 100644
index 00000000..edf509a1
--- /dev/null
+++ b/desktop/src/renderer/src/pages/SettingsPage.tsx
@@ -0,0 +1,60 @@
+import React, { useState } from 'react'
+import { useLocation } from 'react-router-dom'
+import { t } from '../i18n'
+import BasicSettings from './settings/BasicSettings'
+import ModelsTab from './settings/ModelsTab'
+
+interface SettingsPageProps {
+ baseUrl: string
+ onLangChange?: () => void
+}
+
+type Tab = 'basic' | 'models'
+
+const SettingsPage: React.FC = ({ baseUrl, onLangChange }) => {
+ const location = useLocation()
+ // Allow deep-linking to the models tab via /settings?tab=models.
+ const initial: Tab = new URLSearchParams(location.search).get('tab') === 'models' ? 'models' : 'basic'
+ const [tab, setTab] = useState(initial)
+
+ const tabs: { key: Tab; label: string }[] = [
+ { key: 'basic', label: t('settings_tab_basic') },
+ { key: 'models', label: t('settings_tab_models') },
+ ]
+
+ return (
+
+
+
+
{t('menu_settings')}
+
{t('config_desc')}
+
+
+ {/* Tab bar */}
+
+ {tabs.map((tb) => (
+ setTab(tb.key)}
+ className={`relative px-4 py-2.5 text-sm font-medium cursor-pointer transition-colors -mb-px border-b-2 ${
+ tab === tb.key
+ ? 'text-accent border-accent'
+ : 'text-content-tertiary border-transparent hover:text-content-secondary'
+ }`}
+ >
+ {tb.label}
+
+ ))}
+
+
+ {tab === 'basic' ? (
+
setTab('models')} />
+ ) : (
+
+ )}
+
+
+ )
+}
+
+export default SettingsPage
diff --git a/desktop/src/renderer/src/pages/SkillsPage.tsx b/desktop/src/renderer/src/pages/SkillsPage.tsx
new file mode 100644
index 00000000..11863ec8
--- /dev/null
+++ b/desktop/src/renderer/src/pages/SkillsPage.tsx
@@ -0,0 +1,142 @@
+import React, { useEffect, useState } from 'react'
+import { Loader2, Wrench, Zap, Puzzle } from 'lucide-react'
+import { t } from '../i18n'
+import apiClient from '../api/client'
+import type { ToolInfo, SkillInfo } from '../types'
+import { Toggle } from './settings/primitives'
+
+interface SkillsPageProps {
+ baseUrl: string
+}
+
+const SKILL_HUB_URL = 'https://skills.cowagent.ai/'
+
+const SkillsPage: React.FC = ({ baseUrl }) => {
+ const [tools, setTools] = useState([])
+ const [skills, setSkills] = useState([])
+ const [loading, setLoading] = useState(true)
+
+ const loadData = async () => {
+ try {
+ setLoading(true)
+ const [toolsData, skillsData] = await Promise.all([apiClient.getTools(), apiClient.getSkills()])
+ setTools(toolsData || [])
+ setSkills(skillsData || [])
+ } catch (err) {
+ console.error('Failed to load skills:', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ apiClient.setBaseUrl(baseUrl)
+ void loadData()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [baseUrl])
+
+ const toggle = async (skill: SkillInfo, enabled: boolean) => {
+ // Optimistic flip; revert on failure.
+ setSkills((prev) => prev.map((s) => (s.name === skill.name ? { ...s, enabled } : s)))
+ try {
+ const res = await apiClient.toggleSkill(skill.name, enabled ? 'open' : 'close')
+ if (res.status !== 'success') throw new Error()
+ } catch {
+ setSkills((prev) => prev.map((s) => (s.name === skill.name ? { ...s, enabled: !enabled } : s)))
+ }
+ }
+
+ return (
+
+
+
+
+
+ {loading ? (
+
+
+ {t('skills_loading')}
+
+ ) : (
+
+
+ {tools.length === 0 ? (
+
+ ) : (
+
+ {tools.map((tool) => (
+
+
+
+ {tool.name}
+
+
{tool.description || '--'}
+
+ ))}
+
+ )}
+
+
+
+ {skills.length === 0 ? (
+
+ ) : (
+
+ {skills.map((skill) => (
+
+
+
+
+
+
+
+ {skill.display_name || skill.name}
+
+ toggle(skill, v)} />
+
+
{skill.description || '--'}
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+
+
+ )
+}
+
+const Section: React.FC<{ title: string; count: number; children: React.ReactNode }> = ({ title, count, children }) => (
+
+
+ {title}
+ {count > 0 && (
+ {count}
+ )}
+
+ {children}
+
+)
+
+const Empty: React.FC<{ text: string }> = ({ text }) => (
+ {text}
+)
+
+export default SkillsPage
diff --git a/desktop/src/renderer/src/pages/TasksPage.tsx b/desktop/src/renderer/src/pages/TasksPage.tsx
new file mode 100644
index 00000000..aff099e9
--- /dev/null
+++ b/desktop/src/renderer/src/pages/TasksPage.tsx
@@ -0,0 +1,314 @@
+import React, { useEffect, useState } from 'react'
+import { Loader2, Clock, CalendarClock } from 'lucide-react'
+import { useNavigate } from 'react-router-dom'
+import { t } from '../i18n'
+import apiClient from '../api/client'
+import type { SchedulerTask, TaskSchedule, TaskAction } from '../types'
+import { Modal, Btn, Toggle, TextInput, Dropdown } from './settings/primitives'
+
+interface TasksPageProps {
+ baseUrl: string
+}
+
+// Human-readable schedule summary, mirroring the web console.
+const scheduleSummary = (s: TaskSchedule): string => {
+ if (s.type === 'cron') return s.expression || 'cron'
+ if (s.type === 'interval') {
+ const sec = s.seconds || 0
+ const h = Math.floor(sec / 3600)
+ const m = Math.floor((sec % 3600) / 60)
+ const r = sec % 60
+ const parts: string[] = []
+ if (h) parts.push(`${h}h`)
+ if (m) parts.push(`${m}m`)
+ if (r || parts.length === 0) parts.push(`${r}s`)
+ return parts.join(' ')
+ }
+ return s.type || 'once'
+}
+
+const formatNextRun = (iso?: string): string => {
+ if (!iso) return '--'
+ const d = new Date(iso)
+ return isNaN(d.getTime()) ? '--' : d.toLocaleString()
+}
+
+const TasksPage: React.FC = ({ baseUrl }) => {
+ const navigate = useNavigate()
+ const [tasks, setTasks] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [editing, setEditing] = useState(null)
+
+ const loadTasks = async () => {
+ try {
+ setLoading(true)
+ const data = await apiClient.getSchedulerTasks()
+ setTasks(data || [])
+ } catch (err) {
+ console.error('Failed to load tasks:', err)
+ setTasks([])
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ apiClient.setBaseUrl(baseUrl)
+ void loadTasks()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [baseUrl])
+
+ const toggle = async (task: SchedulerTask, enabled: boolean) => {
+ // Optimistic flip; revert on failure.
+ setTasks((prev) => prev.map((x) => (x.id === task.id ? { ...x, enabled } : x)))
+ try {
+ await apiClient.toggleTask(task.id, enabled)
+ } catch {
+ setTasks((prev) => prev.map((x) => (x.id === task.id ? { ...x, enabled: !enabled } : x)))
+ }
+ }
+
+ return (
+
+
+
{t('tasks_title')}
+
{t('tasks_desc')}
+
+
+
+
+ {loading ? (
+
+
+ {t('tasks_loading')}
+
+ ) : tasks.length === 0 ? (
+
+
+
{t('tasks_empty')}
+
{t('tasks_empty_guide')}
+
navigate('/')}
+ className="px-4 py-2 rounded-btn bg-accent text-accent-contrast hover:bg-accent-hover text-sm font-medium cursor-pointer transition-colors"
+ >
+ {t('tasks_go_chat')}
+
+
+ ) : (
+
+ {tasks.map((task) => {
+ const content = task.action?.content || task.action?.task_description || ''
+ return (
+
setEditing(task)}
+ className={`rounded-card border border-default bg-surface p-4 cursor-pointer hover:border-strong transition-colors ${
+ task.enabled ? '' : 'opacity-60'
+ }`}
+ >
+
+
+
{task.name || task.id}
+
+
{scheduleSummary(task.schedule)}
+
+ {content &&
{content}
}
+
e.stopPropagation()}
+ >
+
+
+ {t('tasks_next_run')}: {formatNextRun(task.next_run_at)}
+
+
+
toggle(task, v)} />
+
+
+ )
+ })}
+
+ )}
+
+
+
+ {editing && (
+
setEditing(null)}
+ onSaved={() => {
+ setEditing(null)
+ void loadTasks()
+ }}
+ onDeleted={() => {
+ setEditing(null)
+ void loadTasks()
+ }}
+ />
+ )}
+
+ )
+}
+
+const TaskEditModal: React.FC<{
+ task: SchedulerTask
+ onClose: () => void
+ onSaved: () => void
+ onDeleted: () => void
+}> = ({ task, onClose, onSaved, onDeleted }) => {
+ const [name, setName] = useState(task.name || '')
+ const [enabled, setEnabled] = useState(task.enabled)
+ const [schedType, setSchedType] = useState(task.schedule.type || 'cron')
+ const [cron, setCron] = useState(task.schedule.expression || '')
+ const [interval, setIntervalVal] = useState(task.schedule.seconds ? String(task.schedule.seconds) : '')
+ const [runAt, setRunAt] = useState(task.schedule.run_at ? task.schedule.run_at.slice(0, 16) : '')
+ const [actionType, setActionType] = useState(task.action.type || 'send_message')
+ const [content, setContent] = useState(task.action.content || task.action.task_description || '')
+ const [saving, setSaving] = useState(false)
+ const [error, setError] = useState('')
+
+ const buildSchedule = (): TaskSchedule => {
+ if (schedType === 'cron') return { type: 'cron', expression: cron.trim() }
+ if (schedType === 'interval') return { type: 'interval', seconds: Number(interval) || 0 }
+ return { type: 'once', run_at: runAt }
+ }
+
+ const buildAction = (): TaskAction => {
+ const a: TaskAction = { ...task.action, type: actionType }
+ if (actionType === 'send_message') a.content = content
+ else a.task_description = content
+ return a
+ }
+
+ const save = async () => {
+ setSaving(true)
+ setError('')
+ try {
+ await apiClient.updateTask(task.id, {
+ name: name.trim(),
+ enabled,
+ schedule: buildSchedule(),
+ action: buildAction(),
+ })
+ onSaved()
+ } catch (e) {
+ setError(e instanceof Error ? e.message : t('task_save_error'))
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const del = async () => {
+ if (!window.confirm(t('task_delete_confirm'))) return
+ setSaving(true)
+ try {
+ await apiClient.deleteTask(task.id)
+ onDeleted()
+ } catch {
+ setSaving(false)
+ }
+ }
+
+ return (
+
+
+ {t('task_delete')}
+
+
+ {t('task_cancel')}
+
+
+ {t('task_save')}
+
+ >
+ }
+ >
+
+ setName(e.target.value)} />
+
+
+
+ {t('task_enabled')}
+
+
+
+
+ setSchedType(v as TaskSchedule['type'])}
+ options={[
+ { value: 'cron', label: t('task_type_cron') },
+ { value: 'interval', label: t('task_type_interval') },
+ { value: 'once', label: t('task_type_once') },
+ ]}
+ />
+
+
+ {schedType === 'cron' && (
+
+ setCron(e.target.value)} placeholder="0 9 * * *" className="font-mono" />
+
+ )}
+ {schedType === 'interval' && (
+
+ setIntervalVal(e.target.value)} />
+
+ )}
+ {schedType === 'once' && (
+
+ setRunAt(e.target.value)} />
+
+ )}
+
+
+ setActionType(v as TaskAction['type'])}
+ options={[
+ { value: 'send_message', label: t('task_action_send') },
+ { value: 'agent_task', label: t('task_action_agent') },
+ ]}
+ />
+
+
+
+
+
+ {/* Channel and receiver are channel-bound and read-only after creation. */}
+ {(task.action.channel_type || task.action.receiver) && (
+
+
+
+
+
+
+
+
+ )}
+ {t('task_channel_locked')}
+
+ {error && {error}
}
+
+ )
+}
+
+const Field: React.FC<{ label: string; hint?: string; children: React.ReactNode }> = ({ label, hint, children }) => (
+
+
{label}
+ {children}
+ {hint &&
{hint}
}
+
+)
+
+export default TasksPage
diff --git a/desktop/src/renderer/src/pages/settings/BasicSettings.tsx b/desktop/src/renderer/src/pages/settings/BasicSettings.tsx
new file mode 100644
index 00000000..b2ff37a6
--- /dev/null
+++ b/desktop/src/renderer/src/pages/settings/BasicSettings.tsx
@@ -0,0 +1,331 @@
+import React, { useState, useEffect } from 'react'
+import { Cpu, Bot, ShieldCheck, Languages, Eye, EyeOff, ArrowRight, Loader2 } from 'lucide-react'
+import { t, getLang, setLang, localizedLabel, type Lang } from '../../i18n'
+import apiClient from '../../api/client'
+import type { ConfigData, ProviderMeta } from '../../types'
+import { Card, Field, Dropdown, Toggle, TextInput, SaveRow, MASK_RE } from './primitives'
+
+interface BasicSettingsProps {
+ baseUrl: string
+ onLangChange?: () => void
+ onOpenModels?: () => void
+}
+
+const BasicSettings: React.FC = ({ baseUrl, onLangChange, onOpenModels }) => {
+ const [config, setConfig] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ // model card — credentials (key/base) now live in the Models tab
+ const [provider, setProvider] = useState('')
+ const [model, setModel] = useState('')
+ const [customModel, setCustomModel] = useState('')
+ const [showCustom, setShowCustom] = useState(false)
+ const [modelStatus, setModelStatus] = useState('')
+
+ // agent card
+ const [maxTokens, setMaxTokens] = useState(100000)
+ const [maxTurns, setMaxTurns] = useState(20)
+ const [maxSteps, setMaxSteps] = useState(20)
+ const [thinking, setThinking] = useState(false)
+ const [evolution, setEvolution] = useState(false)
+ const [agentStatus, setAgentStatus] = useState('')
+
+ // security card
+ const [password, setPassword] = useState('')
+ const [pwDirty, setPwDirty] = useState(false)
+ const [pwVisible, setPwVisible] = useState(false)
+ const [pwStatus, setPwStatus] = useState('')
+
+ useEffect(() => {
+ apiClient.setBaseUrl(baseUrl)
+ loadConfig()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [baseUrl])
+
+ const providerMeta = (id: string): ProviderMeta | undefined => config?.providers?.[id] as ProviderMeta | undefined
+
+ const loadConfig = async () => {
+ try {
+ setLoading(true)
+ const data = await apiClient.getConfig()
+ setConfig(data)
+ setModel(data.model || '')
+ setMaxTokens(data.agent_max_context_tokens ?? 100000)
+ setMaxTurns(data.agent_max_context_turns ?? 20)
+ setMaxSteps(data.agent_max_steps ?? 20)
+ setThinking(!!data.enable_thinking)
+ setEvolution(!!data.self_evolution_enabled)
+ setPassword(data.web_password_masked || '')
+ setPwDirty(false)
+
+ const ids = data.providers ? Object.keys(data.providers) : []
+ const current = data.use_linkai ? 'linkai' : data.bot_type || ids[0] || ''
+ setProvider(current)
+ const meta = data.providers?.[current] as ProviderMeta | undefined
+ const presets = meta?.models || []
+ if (data.model && presets.length && !presets.includes(data.model)) {
+ setShowCustom(true)
+ setCustomModel(data.model)
+ }
+ } catch (err) {
+ console.error('Failed to load config:', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleProviderChange = (id: string) => {
+ setProvider(id)
+ setShowCustom(false)
+ setCustomModel('')
+ if (config) {
+ const meta = config.providers?.[id] as ProviderMeta | undefined
+ const models = meta?.models || []
+ setModel(models[0] || '')
+ }
+ }
+
+ const handleModelChange = (val: string) => {
+ if (val === '__custom__') {
+ setShowCustom(true)
+ setModel('')
+ } else {
+ setShowCustom(false)
+ setModel(val)
+ setCustomModel('')
+ }
+ }
+
+ const saveModelConfig = async () => {
+ const finalModel = showCustom ? customModel.trim() : model
+ const isLinkai = provider === 'linkai'
+ try {
+ await apiClient.updateConfig({
+ model: finalModel,
+ use_linkai: isLinkai,
+ bot_type: isLinkai ? '' : provider,
+ })
+ setModelStatus(t('config_saved'))
+ const fresh = await apiClient.getConfig()
+ setConfig(fresh)
+ } catch {
+ setModelStatus(t('config_save_error'))
+ }
+ setTimeout(() => setModelStatus(''), 2000)
+ }
+
+ const saveAgentConfig = async () => {
+ try {
+ await apiClient.updateConfig({
+ agent_max_context_tokens: maxTokens,
+ agent_max_context_turns: maxTurns,
+ agent_max_steps: maxSteps,
+ enable_thinking: thinking,
+ self_evolution_enabled: evolution,
+ })
+ setAgentStatus(t('config_saved'))
+ } catch {
+ setAgentStatus(t('config_save_error'))
+ }
+ setTimeout(() => setAgentStatus(''), 2000)
+ }
+
+ const savePassword = async () => {
+ if (!pwDirty || MASK_RE.test(password)) return
+ try {
+ await apiClient.updateConfig({ web_password: password })
+ setPwStatus(password ? t('config_password_saved') : t('config_password_cleared'))
+ setPwDirty(false)
+ } catch {
+ setPwStatus(t('config_save_error'))
+ }
+ setTimeout(() => setPwStatus(''), 3000)
+ }
+
+ const changeLanguage = async (lang: Lang) => {
+ setLang(lang)
+ onLangChange?.()
+ try {
+ await apiClient.updateConfig({ cow_lang: lang })
+ } catch {
+ /* non-blocking */
+ }
+ }
+
+ if (loading) {
+ return (
+
+
+ {t('skills_loading')}
+
+ )
+ }
+
+ // A provider counts as configured when its key field holds a value.
+ // Custom providers (no key field) carry their own credential, so treat as configured.
+ const isConfigured = (id: string): boolean => {
+ const meta = providerMeta(id)
+ const f = meta?.api_key_field
+ if (!f) return true
+ return !!config?.api_keys?.[f]
+ }
+
+ const providerIds = config?.providers ? Object.keys(config.providers) : []
+ const providerOptions = providerIds.map((id) => ({
+ value: id,
+ label: localizedLabel(providerMeta(id)?.label) || id,
+ hint: isConfigured(id) ? undefined : t('config_provider_unconfigured'),
+ }))
+ const currentMeta = providerMeta(provider)
+ const currentUnconfigured = !!provider && !isConfigured(provider)
+ const modelOptions = [
+ ...(currentMeta?.models || []).map((m) => ({ value: m, label: m })),
+ { value: '__custom__', label: t('config_custom_option') },
+ ]
+
+ return (
+
+ {/* Model — provider/model selection only; credentials live in Models tab */}
+
} title={t('config_model')}>
+
+
+
+
+
+
+ {showCustom && (
+ setCustomModel(e.target.value)}
+ placeholder={t('config_custom_model_hint')}
+ />
+ )}
+
+
+ {/* Guide users to the Models tab for API key / base config.
+ When the selected provider has no credentials, surface a warning. */}
+ {onOpenModels && (
+
+
+ {currentUnconfigured ? t('config_provider_unconfigured_hint') : t('config_credentials_link')}
+
+
+ {t('config_goto_models')}
+
+
+
+ )}
+
+
+
+
+
+ {/* Agent */}
+
} title={t('config_agent')}>
+
+
+ setMaxTokens(parseInt(e.target.value) || 0)}
+ />
+
+
+ setMaxTurns(parseInt(e.target.value) || 0)}
+ />
+
+
+ setMaxSteps(parseInt(e.target.value) || 0)}
+ />
+
+
+
+
{t('config_thinking')}
+
{t('config_thinking_hint')}
+
+
+
+
+
+
{t('config_evolution')}
+
{t('config_evolution_hint')}
+
+
+
+
+
+
+
+ {/* Security */}
+
} title={t('config_security')}>
+
+
+
+ {
+ if (!pwDirty && MASK_RE.test(password)) setPassword('')
+ }}
+ onBlur={() => {
+ if (!pwDirty) setPassword(config?.web_password_masked || '')
+ }}
+ onChange={(e) => {
+ setPassword(e.target.value)
+ setPwDirty(true)
+ }}
+ />
+ setPwVisible((v) => !v)}
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 text-content-tertiary hover:text-content-secondary cursor-pointer p-1"
+ >
+ {pwVisible ? : }
+
+
+
+
+
+
+
+ {/* Language */}
+
} title={t('config_language')}>
+
+ changeLanguage(v as Lang)}
+ />
+
+
+
+ )
+}
+
+export default BasicSettings
diff --git a/desktop/src/renderer/src/pages/settings/CapabilityCard.tsx b/desktop/src/renderer/src/pages/settings/CapabilityCard.tsx
new file mode 100644
index 00000000..2da063df
--- /dev/null
+++ b/desktop/src/renderer/src/pages/settings/CapabilityCard.tsx
@@ -0,0 +1,159 @@
+import React, { useMemo, useState } from 'react'
+import type { LucideIcon } from 'lucide-react'
+import { Loader2 } from 'lucide-react'
+import { t } from '../../i18n'
+import type { CapabilityState, ModelsData } from '../../types'
+import { Card, Field, Dropdown, TextInput, type DropdownOption } from './primitives'
+import { resolveModels, providerLabel, CUSTOM_OPTION } from './modelsHelpers'
+
+// Generic provider+model capability card used by chat/vision/asr/embedding/image.
+// tts (voice) and search have bespoke cards.
+
+export interface CapabilityCardProps {
+ icon: LucideIcon
+ title: string
+ subtitle?: string
+ capKey: string
+ state: CapabilityState
+ data: ModelsData | null
+ // whether picking "no provider" (auto / disabled) is allowed
+ allowAuto?: boolean
+ autoLabel?: string
+ // whether to allow a free-form custom model entry
+ allowCustomModel?: boolean
+ busy?: boolean
+ status?: string
+ onSave: (providerId: string, model: string) => void
+ children?: React.ReactNode
+}
+
+const CapabilityCard: React.FC = ({
+ icon: Icon,
+ title,
+ subtitle,
+ state,
+ data,
+ allowAuto,
+ autoLabel,
+ allowCustomModel,
+ busy,
+ status,
+ onSave,
+ children,
+}) => {
+ const [provider, setProvider] = useState(state.current_provider || '')
+ const [model, setModel] = useState(state.current_model || '')
+ const [customModel, setCustomModel] = useState('')
+ const [showCustom, setShowCustom] = useState(false)
+
+ // A provider is configured when it has credentials (custom providers always
+ // carry their own). Unconfigured ones stay selectable but are flagged so the
+ // user is guided to set up the API key.
+ const isConfigured = (id: string): boolean => {
+ const p = data?.providers?.find((x) => x.id === id)
+ if (!p) return true
+ return p.configured || (p.is_custom && !!p.custom_name)
+ }
+
+ const providerOptions: DropdownOption[] = useMemo(() => {
+ const opts = (state.providers || []).map((id) => ({
+ value: id,
+ label: providerLabel(data, id),
+ hint: isConfigured(id) ? undefined : t('config_provider_unconfigured'),
+ }))
+ if (allowAuto) return [{ value: '', label: autoLabel || t('models_auto') }, ...opts]
+ return opts
+ }, [state.providers, data, allowAuto, autoLabel])
+
+ const currentUnconfigured = !!provider && !isConfigured(provider)
+
+ const modelOptions: DropdownOption[] = useMemo(() => {
+ const list = resolveModels(data, provider, state.provider_models).map((o) => ({
+ value: o.value,
+ label: o.value,
+ hint: o.hint,
+ }))
+ // Keep the currently-saved model selectable even if it's not in the preset list.
+ if (model && !showCustom && !list.some((o) => o.value === model)) {
+ list.unshift({ value: model, label: model, hint: undefined })
+ }
+ if (allowCustomModel) list.push({ value: CUSTOM_OPTION, label: t('config_custom_option'), hint: undefined })
+ return list
+ }, [data, state.provider_models, provider, allowCustomModel, model, showCustom])
+
+ const handleProvider = (id: string) => {
+ setProvider(id)
+ setShowCustom(false)
+ setCustomModel('')
+ const first = resolveModels(data, id, state.provider_models)[0]
+ setModel(first?.value || '')
+ }
+
+ const handleModel = (val: string) => {
+ if (val === CUSTOM_OPTION) {
+ setShowCustom(true)
+ setModel('')
+ } else {
+ setShowCustom(false)
+ setModel(val)
+ setCustomModel('')
+ }
+ }
+
+ const finalModel = showCustom ? customModel.trim() : model
+ const isAuto = allowAuto && !provider
+
+ return (
+ } title={title} subtitle={subtitle}>
+
+
+
+ {/* The provider's API key is configured in the vendor cards above on
+ this same tab, so warn instead of linking elsewhere. */}
+ {currentUnconfigured && (
+ {t('config_provider_unconfigured_hint')}
+ )}
+
+ {!isAuto && (
+
+
+ {showCustom && (
+ setCustomModel(e.target.value)}
+ placeholder={t('config_custom_model_hint')}
+ />
+ )}
+
+ )}
+ {children}
+
+
+ {status}
+
+ onSave(provider, finalModel)}
+ className="px-4 py-2 rounded-btn bg-accent text-accent-contrast hover:bg-accent-hover text-sm font-medium cursor-pointer transition-colors disabled:opacity-50 inline-flex items-center gap-2"
+ >
+ {busy && }
+ {t('config_save')}
+
+
+
+
+ )
+}
+
+export default CapabilityCard
diff --git a/desktop/src/renderer/src/pages/settings/ModelsTab.tsx b/desktop/src/renderer/src/pages/settings/ModelsTab.tsx
new file mode 100644
index 00000000..f3966373
--- /dev/null
+++ b/desktop/src/renderer/src/pages/settings/ModelsTab.tsx
@@ -0,0 +1,833 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
+import {
+ MessageSquare,
+ Eye,
+ Image as ImageIcon,
+ Mic,
+ Volume2,
+ Database,
+ Search as SearchIcon,
+ Plus,
+ Check,
+ Loader2,
+ Pencil,
+ Eye as EyeIcon,
+ EyeOff,
+} from 'lucide-react'
+import { t, localizedLabel } from '../../i18n'
+import apiClient from '../../api/client'
+import type { CapabilityState, ModelsData, ModelProvider, SearchCapabilityState } from '../../types'
+import { Card, Field, Dropdown, TextInput, Modal, Btn, MASK_RE } from './primitives'
+import CapabilityCard from './CapabilityCard'
+import { normEntries, providerLabel, resolveVoices, CUSTOM_OPTION } from './modelsHelpers'
+
+interface ModelsTabProps {
+ baseUrl: string
+}
+
+const REPLY_MODES: { value: 'off' | 'voice_if_voice' | 'always'; key: string }[] = [
+ { value: 'off', key: 'models_tts_mode_off' },
+ { value: 'voice_if_voice', key: 'models_tts_mode_if_voice' },
+ { value: 'always', key: 'models_tts_mode_always' },
+]
+
+const ModelsTab: React.FC = ({ baseUrl }) => {
+ const [data, setData] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [busy, setBusy] = useState('') // capability key currently saving
+ const [statusMap, setStatusMap] = useState>({})
+
+ const load = useCallback(async () => {
+ try {
+ const fresh = await apiClient.getModels()
+ setData(fresh)
+ } catch (e) {
+ console.error('Failed to load models:', e)
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ apiClient.setBaseUrl(baseUrl)
+ load()
+ }, [baseUrl, load])
+
+ const flash = (key: string, msg: string) => {
+ setStatusMap((m) => ({ ...m, [key]: msg }))
+ setTimeout(() => setStatusMap((m) => ({ ...m, [key]: '' })), 2000)
+ }
+
+ // Run a models action, then refresh and flash a status for the given key.
+ const run = async (key: string, action: Parameters[0]) => {
+ setBusy(key)
+ try {
+ const res = await apiClient.modelsAction(action)
+ if (res.status === 'success') {
+ await load()
+ flash(key, t('config_saved'))
+ } else {
+ flash(key, (res.message as string) || t('config_save_error'))
+ }
+ } catch {
+ flash(key, t('config_save_error'))
+ } finally {
+ setBusy('')
+ }
+ }
+
+ if (loading) {
+ return (
+
+
+ {t('skills_loading')}
+
+ )
+ }
+ if (!data) {
+ return {t('config_save_error')}
+ }
+
+ const caps = data.capabilities
+
+ return (
+
+
+
+ {/* Chat */}
+ run('chat', { action: 'set_capability', capability: 'chat', provider_id: p, model: m })}
+ />
+
+ {/* Vision */}
+ run('vision', { action: 'set_capability', capability: 'vision', provider_id: p, model: m })}
+ >
+
+
+
+ {/* Image */}
+ run('image', { action: 'set_capability', capability: 'image', provider_id: p, model: m })}
+ >
+
+
+
+ {/* ASR */}
+ run('asr', { action: 'set_capability', capability: 'asr', provider_id: p, model: m })}
+ />
+
+ {/* TTS — bespoke (voice + reply mode) */}
+
+ run('tts', { action: 'set_capability', capability: 'tts', provider_id: p, model: m, voice: v })
+ }
+ onSaveMode={(mode) => run('tts_mode', { action: 'set_voice_reply_mode', mode })}
+ modeStatus={statusMap.tts_mode}
+ modeBusy={busy === 'tts_mode'}
+ />
+
+ {/* Embedding */}
+ run('embedding', { action: 'set_capability', capability: 'embedding', provider_id: p, model: m })}
+ />
+
+ {/* Search — bespoke */}
+
+ run('search', { action: 'set_capability', capability: 'search', strategy, provider })
+ }
+ onSaveBochaKey={(key) => run('search_key', { action: 'set_search_credential', api_key: key })}
+ keyStatus={statusMap.search_key}
+ keyBusy={busy === 'search_key'}
+ />
+
+ )
+}
+
+// ============================================================
+// Layer 1 — vendor credentials
+// ============================================================
+
+interface VendorSectionProps {
+ data: ModelsData
+ onChanged: () => Promise
+ statusMap: Record
+ flash: (key: string, msg: string) => void
+}
+
+const VendorSection: React.FC = ({ data, onChanged }) => {
+ // Edit an existing built-in vendor.
+ const [editing, setEditing] = useState(null)
+ // Add flow: open the vendor modal with a provider picker.
+ const [adding, setAdding] = useState(false)
+ // Custom provider modal: 'new' to create, or a provider to edit.
+ const [customEditing, setCustomEditing] = useState(null)
+
+ const isCustomCard = (p: ModelProvider) => p.is_custom && !!p.custom_name
+ // Unified grid: configured built-ins + all custom provider cards (web parity).
+ const shown = data.providers.filter((p) => p.configured || isCustomCard(p))
+
+ return (
+ } title={t('models_vendors')} subtitle={t('models_vendors_sub')}>
+ {shown.length === 0 ? (
+
+
{t('models_no_vendor')}
+
setAdding(true)}
+ className="mt-3 inline-flex items-center gap-1 px-3 py-1.5 rounded-btn text-xs font-medium bg-accent-soft text-accent hover:bg-accent-soft/70 cursor-pointer transition-colors"
+ >
+ {t('models_add_vendor')}
+
+
+ ) : (
+
+ {shown.map((p) =>
+ isCustomCard(p) ? (
+
setCustomEditing(p)} />
+ ) : (
+ setEditing(p)} />
+ )
+ )}
+ setAdding(true)}
+ className="flex items-center justify-center gap-1.5 px-3 py-2.5 rounded-btn border border-dashed border-default text-content-tertiary hover:border-accent hover:text-accent cursor-pointer transition-colors text-sm"
+ >
+ {t('models_add_vendor')}
+
+
+ )}
+
+ {
+ setEditing(null)
+ setAdding(false)
+ }}
+ onPickCustom={() => {
+ setAdding(false)
+ setCustomEditing('new')
+ }}
+ onSaved={onChanged}
+ />
+ setCustomEditing(null)} onSaved={onChanged} />
+
+ )
+}
+
+const VendorChip: React.FC<{ provider: ModelProvider; onClick: () => void }> = ({ provider, onClick }) => (
+
+
+ {(localizedLabel(provider.label) || provider.id || '?').slice(0, 1).toUpperCase()}
+
+ {localizedLabel(provider.label)}
+
+
+)
+
+const CUSTOM_PICK = '__custom_new__'
+
+const VendorModal: React.FC<{
+ provider: ModelProvider | null
+ addMode: boolean
+ data: ModelsData
+ onClose: () => void
+ onPickCustom: () => void
+ onSaved: () => Promise
+}> = ({ provider, addMode, data, onClose, onPickCustom, onSaved }) => {
+ const open = !!provider || addMode
+
+ // In add-mode the user first picks a built-in provider; that selection
+ // becomes the effective provider whose key/base fields we edit.
+ const builtins = useMemo(() => data.providers.filter((p) => !(p.is_custom && p.custom_name)), [data.providers])
+ const firstUnconfigured = builtins.find((p) => !p.configured) || builtins[0]
+ const [pickId, setPickId] = useState('')
+
+ const effective: ModelProvider | undefined = provider || builtins.find((p) => p.id === pickId)
+
+ const [apiKey, setApiKey] = useState('')
+ const [keyDirty, setKeyDirty] = useState(false)
+ const [keyVisible, setKeyVisible] = useState(false)
+ const [apiBase, setApiBase] = useState('')
+ const [saving, setSaving] = useState(false)
+
+ // Load fields whenever the effective provider changes.
+ useEffect(() => {
+ if (!open) return
+ const init = provider || (addMode ? firstUnconfigured : undefined)
+ setPickId(provider ? provider.id : firstUnconfigured?.id || '')
+ setApiKey(init?.api_key_masked || '')
+ setApiBase(init?.api_base || '')
+ setKeyDirty(false)
+ setKeyVisible(false)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [provider, addMode, open])
+
+ if (!open) return null
+
+ const pickOptions = [
+ ...builtins.map((p) => ({
+ value: p.id,
+ label: localizedLabel(p.label),
+ hint: p.configured ? t('models_configured') : undefined,
+ })),
+ { value: CUSTOM_PICK, label: t('models_custom_vendor'), hint: t('models_add_custom_hint') },
+ ]
+
+ const onPick = (val: string) => {
+ if (val === CUSTOM_PICK) {
+ onPickCustom()
+ return
+ }
+ setPickId(val)
+ const p = builtins.find((x) => x.id === val)
+ setApiKey(p?.api_key_masked || '')
+ setApiBase(p?.api_base || '')
+ setKeyDirty(false)
+ }
+
+ const hasBase = !!effective?.api_base_field
+
+ const save = async () => {
+ if (!effective) return
+ setSaving(true)
+ try {
+ const payload: { action: 'set_provider'; provider_id: string; api_key?: string; api_base?: string } = {
+ action: 'set_provider',
+ provider_id: effective.id,
+ }
+ if (keyDirty && apiKey && !MASK_RE.test(apiKey)) payload.api_key = apiKey
+ if (hasBase) payload.api_base = apiBase
+ await apiClient.modelsAction(payload)
+ await onSaved()
+ onClose()
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const clear = async () => {
+ if (!effective || !confirm(t('models_clear_confirm'))) return
+ setSaving(true)
+ try {
+ await apiClient.modelsAction({ action: 'delete_provider', provider_id: effective.id })
+ await onSaved()
+ onClose()
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ return (
+
+ {!addMode && effective?.configured && (
+
+ {t('models_clear')}
+
+ )}
+
+ {t('config_cancel')}
+
+
+ {saving ? : t('config_save')}
+
+ >
+ }
+ >
+ {addMode && (
+
+
+
+ )}
+
+
+ {
+ if (!keyDirty && MASK_RE.test(apiKey)) setApiKey('')
+ }}
+ onBlur={() => {
+ if (!keyDirty) setApiKey(effective?.api_key_masked || '')
+ }}
+ onChange={(e) => {
+ setApiKey(e.target.value)
+ setKeyDirty(true)
+ }}
+ />
+ setKeyVisible((v) => !v)}
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 text-content-tertiary hover:text-content-secondary cursor-pointer p-1"
+ >
+ {keyVisible ? : }
+
+
+
+ {hasBase && (
+
+ setApiBase(e.target.value)}
+ placeholder={effective?.api_base_placeholder || 'https://...'}
+ />
+
+ )}
+
+ )
+}
+
+const CustomProviderModal: React.FC<{
+ target: ModelProvider | 'new' | null
+ onClose: () => void
+ onSaved: () => Promise
+}> = ({ target, onClose, onSaved }) => {
+ const editing = target && target !== 'new' ? target : null
+ const [name, setName] = useState('')
+ const [apiBase, setApiBase] = useState('')
+ const [apiKey, setApiKey] = useState('')
+ const [keyDirty, setKeyDirty] = useState(false)
+ const [saving, setSaving] = useState(false)
+
+ useEffect(() => {
+ if (!target) return
+ if (editing) {
+ setName(editing.custom_name || localizedLabel(editing.label))
+ setApiBase(editing.api_base || '')
+ setApiKey(editing.api_key_masked || '')
+ } else {
+ setName('')
+ setApiBase('')
+ setApiKey('')
+ }
+ setKeyDirty(false)
+ }, [target, editing])
+
+ if (!target) return null
+
+ const save = async () => {
+ if (!name.trim()) return
+ setSaving(true)
+ try {
+ const payload: {
+ action: 'set_custom_provider'
+ name: string
+ id?: string
+ api_base: string
+ api_key?: string
+ } = {
+ action: 'set_custom_provider',
+ name: name.trim(),
+ api_base: apiBase.trim(),
+ }
+ if (editing) payload.id = editing.custom_id
+ if (keyDirty && apiKey && !MASK_RE.test(apiKey)) payload.api_key = apiKey
+ await apiClient.modelsAction(payload)
+ await onSaved()
+ onClose()
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const remove = async () => {
+ if (!editing || !confirm(t('models_delete_confirm'))) return
+ setSaving(true)
+ try {
+ await apiClient.modelsAction({ action: 'delete_custom_provider', id: editing.custom_id || '' })
+ await onSaved()
+ onClose()
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ return (
+
+ {editing && (
+
+ {t('models_delete')}
+
+ )}
+
+ {t('config_cancel')}
+
+
+ {saving ? : t('config_save')}
+
+ >
+ }
+ >
+
+ setName(e.target.value)} placeholder="My Provider" />
+
+
+ setApiBase(e.target.value)}
+ placeholder="https://...../v1"
+ />
+
+
+ {
+ if (!keyDirty && MASK_RE.test(apiKey)) setApiKey('')
+ }}
+ onChange={(e) => {
+ setApiKey(e.target.value)
+ setKeyDirty(true)
+ }}
+ />
+
+
+ )
+}
+
+// ============================================================
+// Bespoke capability cards
+// ============================================================
+
+const FallbackHint: React.FC<{ state: CapabilityState; data: ModelsData }> = ({ state, data }) => {
+ if (!state.fallback_provider && !state.fallback_model) return null
+ const label = providerLabel(data, state.fallback_provider || '')
+ return (
+
+ {t('models_fallback')}: {label} {state.fallback_model ? `· ${state.fallback_model}` : ''}
+
+ )
+}
+
+const TtsCard: React.FC<{
+ state: CapabilityState
+ data: ModelsData
+ busy: boolean
+ status?: string
+ onSaveVoice: (provider: string, model: string, voice: string) => void
+ onSaveMode: (mode: 'off' | 'voice_if_voice' | 'always') => void
+ modeStatus?: string
+ modeBusy: boolean
+}> = ({ state, data, busy, status, onSaveVoice, onSaveMode, modeStatus, modeBusy }) => {
+ const [provider, setProvider] = useState(state.current_provider || '')
+ const [model, setModel] = useState(state.current_model || '')
+ const [voice, setVoice] = useState(state.current_voice || '')
+ const [mode, setMode] = useState<'off' | 'voice_if_voice' | 'always'>(state.reply_mode || 'off')
+
+ const providerOptions = (state.providers || []).map((id) => ({ value: id, label: providerLabel(data, id) }))
+ const modelOptions = normEntries(state.provider_models?.[provider]).map((o) => ({
+ value: o.value,
+ label: o.value,
+ hint: o.hint,
+ }))
+ const voiceOptions = resolveVoices(provider, model, state.provider_voices).map((o) => ({
+ value: o.value,
+ label: o.value,
+ hint: o.hint,
+ }))
+
+ const handleProvider = (id: string) => {
+ setProvider(id)
+ const first = normEntries(state.provider_models?.[id])[0]
+ const fm = first?.value || ''
+ setModel(fm)
+ setVoice(resolveVoices(id, fm, state.provider_voices)[0]?.value || '')
+ }
+ const handleModel = (m: string) => {
+ setModel(m)
+ setVoice(resolveVoices(provider, m, state.provider_voices)[0]?.value || '')
+ }
+
+ return (
+ } title={t('models_cap_tts')} subtitle={t('models_cap_tts_sub')}>
+
+ {/* Reply mode — saved immediately */}
+
+ ({ value: m.value, label: t(m.key) }))}
+ onChange={(v) => {
+ const next = v as 'off' | 'voice_if_voice' | 'always'
+ setMode(next)
+ onSaveMode(next)
+ }}
+ disabled={modeBusy}
+ />
+ {modeStatus && {modeStatus} }
+
+
+ {mode !== 'off' && (
+ <>
+
+
+
+
+
+
+ {voiceOptions.length > 0 && (
+
+
+
+ )}
+
+
+ {status}
+
+ onSaveVoice(provider, model, voice)}
+ className="px-4 py-2 rounded-btn bg-accent text-accent-contrast hover:bg-accent-hover text-sm font-medium cursor-pointer transition-colors disabled:opacity-50 inline-flex items-center gap-2"
+ >
+ {busy && }
+ {t('config_save')}
+
+
+ >
+ )}
+
+
+ )
+}
+
+const EmbeddingCard: React.FC<{
+ state: CapabilityState
+ data: ModelsData
+ busy: boolean
+ status?: string
+ onSave: (provider: string, model: string) => void
+}> = ({ state, data, busy, status, onSave }) => (
+
+ {state.current_dim != null && (
+
+ {t('models_embedding_dim')}: {state.current_dim} · {t('models_embedding_rebuild_hint')}
+
+ )}
+
+)
+
+const SearchCard: React.FC<{
+ state: SearchCapabilityState
+ busy: boolean
+ status?: string
+ onSaveStrategy: (strategy: string, provider: string) => void
+ onSaveBochaKey: (key: string) => void
+ keyStatus?: string
+ keyBusy: boolean
+}> = ({ state, busy, status, onSaveStrategy, onSaveBochaKey, keyStatus, keyBusy }) => {
+ const [strategy, setStrategy] = useState(state.strategy || 'auto')
+ const [provider, setProvider] = useState(state.fixed_provider || state.current_provider || '')
+ const [bochaOpen, setBochaOpen] = useState(false)
+
+ const providerOptions = useMemo(
+ () => state.providers.map((p) => ({ value: p.id, label: localizedLabel(p.label) })),
+ [state.providers]
+ )
+ const bocha = state.providers.find((p) => p.id === 'bocha')
+
+ return (
+ } title={t('models_cap_search')} subtitle={t('models_cap_search_sub')}>
+
+
+
+
+ {strategy === 'fixed' && (
+
+
+
+ )}
+
+
setBochaOpen(true)}
+ className="text-xs text-accent hover:text-accent-hover cursor-pointer inline-flex items-center gap-1"
+ >
+ {t('models_search_bocha_key')}
+ {bocha?.configured && }
+
+
+
+ {status}
+
+ onSaveStrategy(strategy, provider)}
+ className="px-4 py-2 rounded-btn bg-accent text-accent-contrast hover:bg-accent-hover text-sm font-medium cursor-pointer transition-colors disabled:opacity-50 inline-flex items-center gap-2"
+ >
+ {busy && }
+ {t('config_save')}
+
+
+
+
+
+ setBochaOpen(false)}
+ onSave={(k) => {
+ onSaveBochaKey(k)
+ setBochaOpen(false)
+ }}
+ />
+
+ )
+}
+
+const BochaKeyModal: React.FC<{
+ open: boolean
+ masked: string
+ busy: boolean
+ status?: string
+ onClose: () => void
+ onSave: (key: string) => void
+}> = ({ open, masked, busy, onClose, onSave }) => {
+ const [key, setKey] = useState('')
+ const [dirty, setDirty] = useState(false)
+ useEffect(() => {
+ if (open) {
+ setKey(masked)
+ setDirty(false)
+ }
+ }, [open, masked])
+ return (
+
+
+ {t('config_cancel')}
+
+ onSave(dirty && !MASK_RE.test(key) ? key : '')}>
+ {busy ? : t('config_save')}
+
+ >
+ }
+ >
+
+ {
+ if (!dirty && MASK_RE.test(key)) setKey('')
+ }}
+ onChange={(e) => {
+ setKey(e.target.value)
+ setDirty(true)
+ }}
+ />
+
+
+ )
+}
+
+export default ModelsTab
diff --git a/desktop/src/renderer/src/pages/settings/modelsHelpers.ts b/desktop/src/renderer/src/pages/settings/modelsHelpers.ts
new file mode 100644
index 00000000..24ac31cc
--- /dev/null
+++ b/desktop/src/renderer/src/pages/settings/modelsHelpers.ts
@@ -0,0 +1,57 @@
+import type { ModelEntry, ModelOption, ModelProvider, ModelsData } from '../../types'
+import { localizedLabel } from '../../i18n'
+
+// Normalize a string|{value,hint} entry into a uniform option shape.
+export function normEntry(e: ModelEntry): ModelOption {
+ return typeof e === 'string' ? { value: e } : e
+}
+
+export function normEntries(arr?: ModelEntry[]): ModelOption[] {
+ return (arr || []).map(normEntry)
+}
+
+// Resolve a human label for a provider id, falling back to the id itself.
+// Handles expanded custom ids ("custom:") via the providers overview.
+export function providerLabel(data: ModelsData | null, id: string): string {
+ if (!id) return ''
+ const p = data?.providers?.find((x) => x.id === id)
+ if (p) return localizedLabel(p.label) || id
+ return id
+}
+
+export function findProvider(data: ModelsData | null, id: string): ModelProvider | undefined {
+ return data?.providers?.find((x) => x.id === id)
+}
+
+// Resolve the model list for a capability+provider, mirroring the web console:
+// 1. capability-scoped provider_models[id] (vision/image/asr/tts/embedding)
+// 2. provider_models['custom'] for expanded custom: providers
+// 3. fall back to the vendor's generic models[] (chat has no provider_models)
+export function resolveModels(
+ data: ModelsData | null,
+ providerId: string,
+ providerModels?: Record
+): ModelOption[] {
+ if (!providerId) return []
+ if (providerModels?.[providerId]) return normEntries(providerModels[providerId])
+ if (providerId.startsWith('custom:') && providerModels?.['custom']) {
+ return normEntries(providerModels['custom'])
+ }
+ return normEntries(findProvider(data, providerId)?.models)
+}
+
+// Voices for a tts provider may be a flat list or, for linkai, keyed by model.
+export function resolveVoices(
+ provider: string,
+ model: string,
+ voicesMap?: Record>
+): ModelOption[] {
+ const raw = voicesMap?.[provider]
+ if (!raw) return []
+ if (Array.isArray(raw)) return normEntries(raw)
+ // keyed by model (linkai)
+ const byModel = raw as Record
+ return normEntries(byModel[model] || [])
+}
+
+export const CUSTOM_OPTION = '__custom__'
diff --git a/desktop/src/renderer/src/pages/settings/primitives.tsx b/desktop/src/renderer/src/pages/settings/primitives.tsx
new file mode 100644
index 00000000..cfd9b764
--- /dev/null
+++ b/desktop/src/renderer/src/pages/settings/primitives.tsx
@@ -0,0 +1,196 @@
+import React, { useState, useEffect, useRef } from 'react'
+import { ChevronDown } from 'lucide-react'
+import { t } from '../../i18n'
+
+// Shared presentational building blocks for the settings tabs.
+
+export const Card: React.FC<{ icon: React.ReactNode; title: string; subtitle?: string; children: React.ReactNode }> = ({
+ icon,
+ title,
+ subtitle,
+ children,
+}) => (
+
+
+
{icon}
+
+
{title}
+ {subtitle &&
{subtitle}
}
+
+
+ {children}
+
+)
+
+export const Field: React.FC<{ label: string; hint?: string; children: React.ReactNode }> = ({
+ label,
+ hint,
+ children,
+}) => (
+
+
{label}
+ {children}
+ {hint &&
{hint}
}
+
+)
+
+export interface DropdownOption {
+ value: string
+ label: string
+ hint?: string
+}
+
+export const Dropdown: React.FC<{
+ value: string
+ display?: string
+ placeholder?: string
+ options: DropdownOption[]
+ disabled?: boolean
+ onChange: (val: string) => void
+}> = ({ value, display, placeholder, options, disabled, onChange }) => {
+ const [open, setOpen] = useState(false)
+ const ref = useRef(null)
+ useEffect(() => {
+ const h = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
+ }
+ document.addEventListener('mousedown', h)
+ return () => document.removeEventListener('mousedown', h)
+ }, [])
+ const current = display ?? options.find((o) => o.value === value)?.label ?? ''
+ return (
+
+
!disabled && setOpen((v) => !v)}
+ className={`w-full flex items-center justify-between px-3 py-2 rounded-btn border bg-inset text-sm transition-colors ${
+ disabled
+ ? 'border-default text-content-tertiary cursor-not-allowed opacity-70'
+ : 'border-strong text-content cursor-pointer hover:border-accent'
+ }`}
+ >
+ {current || placeholder || '--'}
+
+
+ {open && (
+
+ {options.length === 0 && (
+
{t('models_no_options')}
+ )}
+ {options.map((o) => (
+
{
+ onChange(o.value)
+ setOpen(false)
+ }}
+ className={`px-3 py-2 text-sm cursor-pointer transition-colors ${
+ o.value === value ? 'bg-accent-soft text-accent' : 'text-content-secondary hover:bg-surface-2'
+ }`}
+ >
+
{o.label}
+ {o.hint &&
{o.hint}
}
+
+ ))}
+
+ )}
+
+ )
+}
+
+export const Toggle: React.FC<{ checked: boolean; onChange: (v: boolean) => void }> = ({ checked, onChange }) => (
+ onChange(!checked)}
+ className={`relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors cursor-pointer ${
+ checked ? 'bg-accent' : 'bg-surface-2 border border-strong'
+ }`}
+ >
+
+
+)
+
+export const TextInput: React.FC> = (props) => (
+
+)
+
+export const SaveRow: React.FC<{ status: string; onSave: () => void; label?: string }> = ({
+ status,
+ onSave,
+ label,
+}) => (
+
+ {status}
+
+ {label ?? t('config_save')}
+
+
+)
+
+export const MASK_RE = /[*•]/
+
+export const Modal: React.FC<{
+ open: boolean
+ title: string
+ onClose: () => void
+ children: React.ReactNode
+ footer?: React.ReactNode
+}> = ({ open, title, onClose, children, footer }) => {
+ if (!open) return null
+ return (
+ {
+ if (e.target === e.currentTarget) onClose()
+ }}
+ >
+
+
+
{title}
+
+ ×
+
+
+
{children}
+ {footer &&
{footer}
}
+
+
+ )
+}
+
+export const Btn: React.FC<
+ React.ButtonHTMLAttributes & { variant?: 'primary' | 'ghost' | 'danger' }
+> = ({ variant = 'ghost', className, children, ...props }) => {
+ const styles =
+ variant === 'primary'
+ ? 'bg-accent text-accent-contrast hover:bg-accent-hover'
+ : variant === 'danger'
+ ? 'bg-danger-soft text-danger hover:bg-danger/15 border border-danger-border'
+ : 'border border-strong text-content-secondary hover:bg-surface-2'
+ return (
+
+ {children}
+
+ )
+}
diff --git a/desktop/src/renderer/src/store/chatStore.ts b/desktop/src/renderer/src/store/chatStore.ts
new file mode 100644
index 00000000..f5cadd8a
--- /dev/null
+++ b/desktop/src/renderer/src/store/chatStore.ts
@@ -0,0 +1,420 @@
+import { create } from 'zustand'
+import apiClient from '../api/client'
+import type { ChatMessage, MessageStep, Attachment, StreamEvent, HistoryMessage } from '../types'
+
+/**
+ * Per-session chat state. Supports parallel sessions: each session keeps its
+ * own message list and active stream, so switching sessions never interrupts a
+ * background run. The active EventSource lives in `streams` (outside React).
+ */
+
+interface SessionRuntime {
+ messages: ChatMessage[]
+ isStreaming: boolean
+ requestId: string | null
+ // history pagination
+ historyPage: number
+ historyHasMore: boolean
+ historyLoaded: boolean
+}
+
+interface ChatState {
+ sessions: Record
+
+ getSession: (sid: string) => SessionRuntime
+ ensureSession: (sid: string) => void
+
+ send: (sid: string, text: string, attachments: Attachment[]) => Promise
+ cancel: (sid: string) => Promise
+ regenerate: (sid: string, botMessageId: string) => Promise
+ editUserMessage: (sid: string, messageId: string) => { text: string; attachments: Attachment[] } | null
+ deleteMessage: (sid: string, userSeq: number, cascade: boolean) => Promise
+
+ loadHistory: (sid: string, page?: number) => Promise
+ clearLocal: (sid: string) => void
+}
+
+// EventSource instances kept outside the store (not serializable).
+const streams: Record = {}
+
+const EMPTY: SessionRuntime = {
+ messages: [],
+ isStreaming: false,
+ requestId: null,
+ historyPage: 0,
+ historyHasMore: false,
+ historyLoaded: false,
+}
+
+function uid(prefix: string): string {
+ return `${prefix}_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`
+}
+
+/**
+ * History keeps the English cancel marker for the LLM; strip it for display so
+ * the bubble shows a clean answer + a dedicated "cancelled" badge instead.
+ */
+function stripCancelMarker(text: string): string {
+ if (!text) return text
+ return text
+ .replace(/_\(Cancelled by user\)_/g, '')
+ .replace(/_\(Cancelled\)_/g, '')
+ .trim()
+}
+
+/** Convert a backend history message into a UI ChatMessage. */
+function historyToMessage(m: HistoryMessage): ChatMessage {
+ if (m.role === 'user') {
+ return {
+ id: uid('user'),
+ role: 'user',
+ content: m.content,
+ timestamp: m.created_at,
+ userSeq: m._seq,
+ }
+ }
+
+ // The backend stores the final answer both as `content` and as the LAST
+ // `content` step. Strip that trailing content step so it isn't rendered
+ // twice (matches the web console's renderStepsHtml logic).
+ const raw = m.steps || []
+ let lastContentIdx = -1
+ for (let i = raw.length - 1; i >= 0; i--) {
+ if (raw[i].type === 'content') {
+ lastContentIdx = i
+ break
+ }
+ }
+ const steps: MessageStep[] = raw
+ .filter((_, i) => i !== lastContentIdx)
+ .map((s) => ({ ...s }))
+ const finalContent = m.content || (lastContentIdx >= 0 ? raw[lastContentIdx].content || '' : '')
+
+ return {
+ id: uid('assistant'),
+ role: 'assistant',
+ content: finalContent,
+ timestamp: m.created_at,
+ steps,
+ reasoning: m.reasoning,
+ kind: m.kind,
+ extras: m.extras,
+ botSeq: m._seq,
+ }
+}
+
+export const useChatStore = create((set, get) => {
+ // --- helpers operating on a single session immutably ---
+ const patchSession = (sid: string, patch: Partial) =>
+ set((st) => ({
+ sessions: { ...st.sessions, [sid]: { ...(st.sessions[sid] || EMPTY), ...patch } },
+ }))
+
+ const patchMessages = (sid: string, fn: (msgs: ChatMessage[]) => ChatMessage[]) =>
+ set((st) => {
+ const cur = st.sessions[sid] || EMPTY
+ return { sessions: { ...st.sessions, [sid]: { ...cur, messages: fn(cur.messages) } } }
+ })
+
+ const updateMsg = (sid: string, id: string, fn: (m: ChatMessage) => ChatMessage) =>
+ patchMessages(sid, (msgs) => msgs.map((m) => (m.id === id ? fn(m) : m)))
+
+ /** Attach an EventSource for a request and wire all SSE events to a bot message. */
+ const attachStream = (sid: string, requestId: string, botId: string) => {
+ const es = apiClient.createSSEStream(requestId)
+ streams[sid] = es
+ let tailTimer: ReturnType | null = null
+
+ const closeStream = () => {
+ if (tailTimer) {
+ clearTimeout(tailTimer)
+ tailTimer = null
+ }
+ es.close()
+ if (streams[sid] === es) delete streams[sid]
+ }
+
+ // Mark the turn as complete: UI becomes interactive again immediately.
+ const completeTurn = () => {
+ patchSession(sid, { isStreaming: false, requestId: null })
+ updateMsg(sid, botId, (m) => ({ ...m, isStreaming: false }))
+ }
+
+ const finishStream = () => {
+ completeTurn()
+ closeStream()
+ }
+
+ es.onmessage = (event) => {
+ let data: StreamEvent
+ try {
+ data = JSON.parse(event.data)
+ } catch {
+ return // keepalive
+ }
+
+ switch (data.type) {
+ case 'reasoning':
+ updateMsg(sid, botId, (m) => ({ ...m, reasoning: (m.reasoning || '') + (data.content || '') }))
+ break
+
+ case 'delta':
+ updateMsg(sid, botId, (m) => ({ ...m, content: m.content + (data.content || '') }))
+ break
+
+ case 'message_end':
+ // Freeze accumulated text as a content step when tool calls follow,
+ // mirroring the web console's interleaved step model.
+ if (data.has_tool_calls) {
+ updateMsg(sid, botId, (m) => {
+ if (!m.content.trim()) return m
+ const steps = [...(m.steps || []), { type: 'content' as const, content: m.content.trim() }]
+ return { ...m, steps, content: '' }
+ })
+ }
+ break
+
+ case 'tool_start':
+ updateMsg(sid, botId, (m) => {
+ // commit any reasoning into a thinking step
+ const steps = [...(m.steps || [])]
+ if (m.reasoning && m.reasoning.trim()) {
+ steps.push({ type: 'thinking', content: m.reasoning.trim() })
+ }
+ steps.push({
+ type: 'tool',
+ id: data.tool_call_id,
+ name: data.tool,
+ arguments: data.arguments,
+ status: 'running',
+ })
+ return { ...m, steps, reasoning: '', content: '' }
+ })
+ break
+
+ case 'tool_progress':
+ updateMsg(sid, botId, (m) => ({
+ ...m,
+ steps: (m.steps || []).map((s) =>
+ s.type === 'tool' && s.id === data.tool_call_id ? { ...s, result: data.content } : s
+ ),
+ }))
+ break
+
+ case 'tool_end':
+ updateMsg(sid, botId, (m) => ({
+ ...m,
+ steps: (m.steps || []).map((s) =>
+ s.type === 'tool' && s.id === data.tool_call_id
+ ? {
+ ...s,
+ status: data.status,
+ result: data.result ?? s.result,
+ execution_time: data.execution_time,
+ is_error: data.status !== 'success',
+ }
+ : s
+ ),
+ }))
+ break
+
+ case 'cancelled':
+ updateMsg(sid, botId, (m) => ({ ...m, isCancelled: true }))
+ break
+
+ case 'done':
+ updateMsg(sid, botId, (m) => {
+ const next = stripCancelMarker(data.content || m.content)
+ return {
+ ...m,
+ content: next,
+ botSeq: data.bot_seq ?? m.botSeq,
+ isStreaming: false,
+ }
+ })
+ // backfill the preceding user message's seq for edit/delete
+ if (data.user_seq != null) {
+ patchMessages(sid, (msgs) => {
+ const idx = msgs.findIndex((m) => m.id === botId)
+ for (let i = idx - 1; i >= 0; i--) {
+ if (msgs[i].role === 'user') {
+ msgs[i] = { ...msgs[i], userSeq: data.user_seq }
+ break
+ }
+ }
+ return [...msgs]
+ })
+ }
+ // The answer is final: free the UI now (don't wait for onerror).
+ completeTurn()
+ // Backend keeps the stream open for a short tail (e.g. TTS audio via
+ // voice_attach). Close it ourselves if nothing else arrives.
+ if (tailTimer) clearTimeout(tailTimer)
+ tailTimer = setTimeout(closeStream, 1500)
+ break
+
+ case 'voice_attach':
+ if (data.audio_url) {
+ updateMsg(sid, botId, (m) => ({
+ ...m,
+ extras: { ...(m.extras || {}), audio: data.audio_url },
+ }))
+ }
+ finishStream()
+ break
+
+ case 'error':
+ updateMsg(sid, botId, (m) => ({ ...m, error: data.message || 'stream error', isStreaming: false }))
+ finishStream()
+ break
+ }
+ }
+
+ es.onerror = () => {
+ // Stream closed (often the normal end after `done`/tail). Finalize.
+ finishStream()
+ }
+ }
+
+ return {
+ sessions: {},
+
+ getSession: (sid) => get().sessions[sid] || EMPTY,
+
+ ensureSession: (sid) => {
+ if (!get().sessions[sid]) patchSession(sid, { ...EMPTY })
+ },
+
+ send: async (sid, text, attachments) => {
+ const userMsg: ChatMessage = {
+ id: uid('user'),
+ role: 'user',
+ content: text,
+ timestamp: Date.now() / 1000,
+ attachments: attachments.length ? attachments : undefined,
+ }
+ const botId = uid('assistant')
+ const botMsg: ChatMessage = {
+ id: botId,
+ role: 'assistant',
+ content: '',
+ timestamp: Date.now() / 1000,
+ steps: [],
+ isStreaming: true,
+ }
+ patchMessages(sid, (msgs) => [...msgs, userMsg, botMsg])
+ patchSession(sid, { isStreaming: true })
+
+ try {
+ const res = await apiClient.sendMessage(sid, text, {
+ stream: true,
+ attachments: attachments.length ? attachments : undefined,
+ })
+ if (res.status === 'success' && res.stream && res.request_id) {
+ patchSession(sid, { requestId: res.request_id })
+ attachStream(sid, res.request_id, botId)
+ } else if (res.inline_reply) {
+ updateMsg(sid, botId, (m) => ({ ...m, content: res.inline_reply || '', isStreaming: false }))
+ patchSession(sid, { isStreaming: false })
+ } else {
+ updateMsg(sid, botId, (m) => ({ ...m, error: 'send failed', isStreaming: false }))
+ patchSession(sid, { isStreaming: false })
+ }
+ } catch (err) {
+ updateMsg(sid, botId, (m) => ({ ...m, error: `${err}`, isStreaming: false }))
+ patchSession(sid, { isStreaming: false })
+ }
+ },
+
+ cancel: async (sid) => {
+ const s = get().sessions[sid]
+ if (!s?.requestId) return
+ try {
+ await apiClient.cancel({ requestId: s.requestId, sessionId: sid })
+ } catch {
+ /* ignore */
+ }
+ },
+
+ regenerate: async (sid, botMessageId) => {
+ const s = get().sessions[sid] || EMPTY
+ const idx = s.messages.findIndex((m) => m.id === botMessageId)
+ if (idx < 0) return
+ // find the user message that produced this bot reply
+ let userMsg: ChatMessage | null = null
+ for (let i = idx - 1; i >= 0; i--) {
+ if (s.messages[i].role === 'user') {
+ userMsg = s.messages[i]
+ break
+ }
+ }
+ if (!userMsg) return
+ // delete the turn on the backend (by the user's seq) then resend
+ if (userMsg.userSeq != null) {
+ try {
+ await apiClient.deleteMessage({ sessionId: sid, userSeq: userMsg.userSeq, deleteUser: true, cascade: true })
+ } catch {
+ /* ignore */
+ }
+ }
+ // drop the user+bot messages locally from idx-? : remove from the user msg onward
+ const userIdx = s.messages.indexOf(userMsg)
+ patchMessages(sid, (msgs) => msgs.slice(0, userIdx))
+ await get().send(sid, userMsg.content, userMsg.attachments || [])
+ },
+
+ editUserMessage: (sid, messageId) => {
+ const s = get().sessions[sid] || EMPTY
+ const msg = s.messages.find((m) => m.id === messageId)
+ if (!msg || msg.role !== 'user') return null
+ const userIdx = s.messages.indexOf(msg)
+ // cascade-delete this turn on the backend
+ if (msg.userSeq != null) {
+ apiClient
+ .deleteMessage({ sessionId: sid, userSeq: msg.userSeq, deleteUser: true, cascade: true })
+ .catch(() => {})
+ }
+ patchMessages(sid, (msgs) => msgs.slice(0, userIdx))
+ return { text: msg.content, attachments: msg.attachments || [] }
+ },
+
+ deleteMessage: async (sid, userSeq, cascade) => {
+ try {
+ await apiClient.deleteMessage({ sessionId: sid, userSeq, deleteUser: true, cascade })
+ } catch {
+ /* ignore */
+ }
+ // reload history to reflect server state
+ await get().loadHistory(sid, 1)
+ },
+
+ loadHistory: async (sid, page = 1) => {
+ try {
+ const res = await apiClient.getHistory(sid, page, 20)
+ const uiMsgs = res.messages.map(historyToMessage)
+ patchSession(sid, {
+ historyPage: res.page,
+ historyHasMore: res.has_more,
+ historyLoaded: true,
+ })
+ if (page === 1) {
+ patchMessages(sid, () => uiMsgs)
+ } else {
+ // older page: prepend
+ patchMessages(sid, (msgs) => [...uiMsgs, ...msgs])
+ }
+ } catch {
+ patchSession(sid, { historyLoaded: true })
+ }
+ },
+
+ clearLocal: (sid) => {
+ const es = streams[sid]
+ if (es) {
+ es.close()
+ delete streams[sid]
+ }
+ patchSession(sid, { ...EMPTY })
+ },
+ }
+})
diff --git a/desktop/src/renderer/src/store/onboardingStore.ts b/desktop/src/renderer/src/store/onboardingStore.ts
new file mode 100644
index 00000000..3da27df6
--- /dev/null
+++ b/desktop/src/renderer/src/store/onboardingStore.ts
@@ -0,0 +1,42 @@
+import { create } from 'zustand'
+
+// Onboarding is config-driven: the wizard auto-opens whenever the chat model
+// isn't configured yet, and stops appearing once it is — no persisted "seen"
+// flag that could strand a user who skipped without finishing setup.
+//
+// `dismissedThisSession` is an in-memory guard so that skipping doesn't
+// immediately re-open the wizard within the same run; it resets on relaunch,
+// so an unconfigured app will guide the user again next time.
+
+interface OnboardingState {
+ // Whether the wizard overlay is currently visible.
+ open: boolean
+ // True if the user skipped/finished during THIS app session (not persisted).
+ dismissedThisSession: boolean
+ // Decide whether to auto-open on launch. Opens only when chat isn't
+ // configured AND it wasn't dismissed earlier this session.
+ maybeOpen: (chatConfigured: boolean) => void
+ // Open manually (e.g. from a "setup guide" entry point later).
+ openWizard: () => void
+ // Finish/skip: close and don't auto-reopen this session.
+ finish: () => void
+ // Close without marking dismissed (rarely used; kept for symmetry).
+ close: () => void
+}
+
+export const useOnboardingStore = create((set) => ({
+ open: false,
+ dismissedThisSession: false,
+
+ maybeOpen: (chatConfigured) =>
+ set((s) => {
+ if (chatConfigured || s.dismissedThisSession) return { open: false }
+ return { open: true }
+ }),
+
+ openWizard: () => set({ open: true }),
+
+ finish: () => set({ open: false, dismissedThisSession: true }),
+
+ close: () => set({ open: false }),
+}))
diff --git a/desktop/src/renderer/src/store/sessionStore.ts b/desktop/src/renderer/src/store/sessionStore.ts
new file mode 100644
index 00000000..ebbd837b
--- /dev/null
+++ b/desktop/src/renderer/src/store/sessionStore.ts
@@ -0,0 +1,86 @@
+import { create } from 'zustand'
+import apiClient from '../api/client'
+import type { SessionItem } from '../types'
+
+const ACTIVE_KEY = 'cow_session_id'
+
+interface SessionState {
+ sessions: SessionItem[]
+ total: number
+ page: number
+ hasMore: boolean
+ loading: boolean
+ activeId: string
+
+ loadSessions: (page?: number) => Promise
+ loadMore: () => Promise
+ setActive: (id: string) => void
+ newSession: () => string
+ rename: (id: string, title: string) => Promise
+ remove: (id: string) => Promise
+}
+
+function genId(): string {
+ return `session_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`
+}
+
+function readActive(): string {
+ return localStorage.getItem(ACTIVE_KEY) || genId()
+}
+
+export const useSessionStore = create((set, get) => ({
+ sessions: [],
+ total: 0,
+ page: 1,
+ hasMore: false,
+ loading: false,
+ activeId: readActive(),
+
+ loadSessions: async (page = 1) => {
+ set({ loading: true })
+ try {
+ const res = await apiClient.getSessions(page, 50)
+ set((s) => ({
+ sessions: page === 1 ? res.sessions : [...s.sessions, ...res.sessions],
+ total: res.total,
+ page: res.page,
+ hasMore: res.has_more,
+ loading: false,
+ }))
+ } catch {
+ set({ loading: false })
+ }
+ },
+
+ loadMore: async () => {
+ const { hasMore, loading, page } = get()
+ if (!hasMore || loading) return
+ await get().loadSessions(page + 1)
+ },
+
+ setActive: (id) => {
+ localStorage.setItem(ACTIVE_KEY, id)
+ set({ activeId: id })
+ },
+
+ newSession: () => {
+ const id = genId()
+ localStorage.setItem(ACTIVE_KEY, id)
+ set({ activeId: id })
+ return id
+ },
+
+ rename: async (id, title) => {
+ await apiClient.renameSession(id, title)
+ set((s) => ({
+ sessions: s.sessions.map((sess) => (sess.session_id === id ? { ...sess, title } : sess)),
+ }))
+ },
+
+ remove: async (id) => {
+ await apiClient.deleteSession(id)
+ set((s) => ({ sessions: s.sessions.filter((sess) => sess.session_id !== id) }))
+ // If we removed the active one, start a fresh session
+ if (get().activeId === id) get().newSession()
+ },
+}))
diff --git a/desktop/src/renderer/src/store/uiStore.ts b/desktop/src/renderer/src/store/uiStore.ts
new file mode 100644
index 00000000..8360e206
--- /dev/null
+++ b/desktop/src/renderer/src/store/uiStore.ts
@@ -0,0 +1,48 @@
+import { create } from 'zustand'
+
+const NAV_KEY = 'cow_nav_collapsed'
+const SESSIONS_KEY = 'cow_sessions_collapsed'
+
+interface UIState {
+ /** Navigation rail collapsed (icon-only) vs expanded (icon + label). */
+ navCollapsed: boolean
+ toggleNav: () => void
+ setNavCollapsed: (v: boolean) => void
+
+ /** Session list panel collapsed (hidden) vs expanded. */
+ sessionsCollapsed: boolean
+ toggleSessions: () => void
+
+ /** Currently active session id (Chat page). */
+ activeSessionId: string | null
+ setActiveSessionId: (id: string | null) => void
+}
+
+function readBool(key: string): boolean {
+ return localStorage.getItem(key) === '1'
+}
+
+export const useUIStore = create((set) => ({
+ navCollapsed: readBool(NAV_KEY),
+ toggleNav: () =>
+ set((s) => {
+ const next = !s.navCollapsed
+ localStorage.setItem(NAV_KEY, next ? '1' : '0')
+ return { navCollapsed: next }
+ }),
+ setNavCollapsed: (v) => {
+ localStorage.setItem(NAV_KEY, v ? '1' : '0')
+ set({ navCollapsed: v })
+ },
+
+ sessionsCollapsed: readBool(SESSIONS_KEY),
+ toggleSessions: () =>
+ set((s) => {
+ const next = !s.sessionsCollapsed
+ localStorage.setItem(SESSIONS_KEY, next ? '1' : '0')
+ return { sessionsCollapsed: next }
+ }),
+
+ activeSessionId: null,
+ setActiveSessionId: (id) => set({ activeSessionId: id }),
+}))
diff --git a/desktop/src/renderer/src/store/updateStore.ts b/desktop/src/renderer/src/store/updateStore.ts
new file mode 100644
index 00000000..10863304
--- /dev/null
+++ b/desktop/src/renderer/src/store/updateStore.ts
@@ -0,0 +1,55 @@
+import { create } from 'zustand'
+import type { UpdateStatus } from '../types'
+
+interface UpdateState {
+ status: UpdateStatus | null
+ /** Latest available version, kept across download progress updates. */
+ version: string | null
+ /** Download progress 0-100 while state === 'downloading'. */
+ percent: number
+ /** User dismissed the badge for this version (don't nag again until next). */
+ dismissedVersion: string | null
+
+ setStatus: (s: UpdateStatus) => void
+ dismiss: () => void
+
+ // Actions proxied to the main process via the preload bridge.
+ download: () => void
+ install: () => void
+}
+
+export const useUpdateStore = create((set, get) => ({
+ status: null,
+ version: null,
+ percent: 0,
+ dismissedVersion: null,
+
+ setStatus: (s) =>
+ set(() => {
+ if (s.state === 'available') return { status: s, version: s.version, percent: 0 }
+ if (s.state === 'downloading') return { status: s, percent: s.percent }
+ if (s.state === 'downloaded') return { status: s, version: s.version, percent: 100 }
+ return { status: s }
+ }),
+
+ dismiss: () => set((st) => ({ dismissedVersion: st.version })),
+
+ download: () => window.electronAPI?.downloadUpdate?.(),
+ install: () => window.electronAPI?.installUpdate?.(),
+}))
+
+// Subscribe to main-process update events. Returns an unsubscribe fn.
+export function initUpdateListener(): (() => void) | undefined {
+ return window.electronAPI?.onUpdateStatus?.((status) => {
+ useUpdateStore.getState().setStatus(status as UpdateStatus)
+ })
+}
+
+// Whether a new version should be surfaced (available/downloading/downloaded
+// and not dismissed for that version).
+export function hasPendingUpdate(state: UpdateState): boolean {
+ const s = state.status
+ if (!s) return false
+ const active = s.state === 'available' || s.state === 'downloading' || s.state === 'downloaded'
+ return active && state.dismissedVersion !== state.version
+}
diff --git a/desktop/src/renderer/src/types.ts b/desktop/src/renderer/src/types.ts
new file mode 100644
index 00000000..50018b4e
--- /dev/null
+++ b/desktop/src/renderer/src/types.ts
@@ -0,0 +1,476 @@
+// ============================================================
+// Electron bridge
+// ============================================================
+
+export interface ElectronAPI {
+ getBackendPort: () => Promise
+ getBackendStatus: () => Promise
+ restartBackend: () => Promise
+ selectDirectory: () => Promise
+ selectFile: (filters?: { name: string; extensions: string[] }[]) => Promise
+ // Listener registrars return an unsubscribe fn for cleanup.
+ onBackendStatus: (callback: (data: BackendStatusEvent) => void) => () => void
+ onBackendLog: (callback: (line: string) => void) => () => void
+ windowMinimize: () => Promise
+ windowMaximize: () => Promise
+ windowClose: () => Promise
+ windowIsMaximized: () => Promise
+ onMaximizeChange: (callback: (maximized: boolean) => void) => () => void
+ onMenuAction?: (callback: (action: string) => void) => () => void
+ // Auto-update
+ checkForUpdate?: () => Promise
+ downloadUpdate?: () => Promise
+ installUpdate?: () => Promise
+ onUpdateStatus?: (callback: (status: UpdateStatus) => void) => () => void
+ platform: string
+ // OS UI language (e.g. "zh-CN"); used to default the language on first run.
+ systemLocale?: string
+}
+
+// Mirrors UpdateStatus in src/main/updater.ts.
+export type UpdateStatus =
+ | { state: 'checking' }
+ | { state: 'available'; version: string; notes?: string }
+ | { state: 'not-available' }
+ | { state: 'downloading'; percent: number }
+ | { state: 'downloaded'; version: string }
+ | { state: 'error'; message: string }
+
+export interface BackendStatusEvent {
+ status: 'ready' | 'error' | 'starting'
+ port?: number
+ error?: string
+}
+
+// ============================================================
+// Chat / messages / streaming
+// ============================================================
+
+export type Role = 'user' | 'assistant'
+
+/** A single ordered step inside an assistant turn (matches backend history). */
+export interface MessageStep {
+ type: 'thinking' | 'content' | 'tool'
+ content?: string
+ // tool step fields
+ id?: string
+ name?: string
+ arguments?: Record
+ result?: string
+ is_error?: boolean
+ status?: string
+ execution_time?: number
+}
+
+/** Local UI message model (superset of backend history message). */
+export interface ChatMessage {
+ id: string
+ role: Role
+ content: string
+ /** Unix seconds. Backend history uses `created_at`; we normalize to `timestamp`. */
+ timestamp: number
+ attachments?: Attachment[]
+ /** Ordered steps (thinking / content / tool). Preferred over legacy toolCalls. */
+ steps?: MessageStep[]
+ /** Legacy live-stream tool events (kept for backward compat during streaming). */
+ toolCalls?: ToolCall[]
+ /** Reasoning text streamed via `reasoning` SSE events. */
+ reasoning?: string
+ /** Sequence numbers from backend (for delete/regenerate). */
+ userSeq?: number
+ botSeq?: number
+ /** Self-evolution bubble flag. */
+ kind?: 'evolution'
+ extras?: Record
+ isStreaming?: boolean
+ isCancelled?: boolean
+ error?: string
+}
+
+export interface Attachment {
+ file_path: string
+ file_name: string
+ file_type: 'image' | 'video' | 'file' | 'directory'
+ preview_url?: string
+}
+
+/** Live tool event during SSE streaming. */
+export interface ToolCall {
+ type: 'tool_start' | 'tool_end' | 'tool_progress'
+ tool: string
+ tool_call_id?: string
+ arguments?: Record
+ result?: string
+ status?: string
+ execution_time?: number
+}
+
+/** All SSE event types emitted on /stream. */
+export type StreamEventType =
+ | 'delta'
+ | 'reasoning'
+ | 'tool_start'
+ | 'tool_progress'
+ | 'tool_end'
+ | 'message_end'
+ | 'phase'
+ | 'file_to_send'
+ | 'image'
+ | 'video'
+ | 'file'
+ | 'text'
+ | 'done'
+ | 'cancelled'
+ | 'voice_attach'
+ | 'error'
+
+export interface StreamEvent {
+ type: StreamEventType
+ content?: string
+ tool?: string
+ tool_call_id?: string
+ arguments?: Record
+ status?: string
+ result?: string
+ execution_time?: number
+ has_tool_calls?: boolean
+ path?: string
+ file_name?: string
+ file_type?: string
+ web_url?: string
+ audio_url?: string
+ request_id?: string
+ timestamp?: number
+ user_seq?: number
+ bot_seq?: number
+ message?: string
+}
+
+// ============================================================
+// Sessions / history
+// ============================================================
+
+export interface SessionItem {
+ session_id: string
+ title: string
+ created_at: number
+ last_active: number
+ msg_count: number
+}
+
+export interface SessionsPage {
+ sessions: SessionItem[]
+ total: number
+ page: number
+ page_size: number
+ has_more: boolean
+}
+
+/** Backend history message (as returned by /api/history). */
+export interface HistoryMessage {
+ role: Role
+ content: string
+ created_at: number
+ steps?: MessageStep[]
+ tool_calls?: Array<{ id?: string; name: string; arguments?: Record; result?: string }>
+ reasoning?: string
+ kind?: 'evolution'
+ extras?: Record
+ /** Per-message sequence number used by delete/regenerate APIs. */
+ _seq?: number
+}
+
+export interface HistoryPage {
+ messages: HistoryMessage[]
+ total: number
+ page: number
+ page_size: number
+ has_more: boolean
+ context_start_seq?: number
+}
+
+// ============================================================
+// Config
+// ============================================================
+
+/** A label that may be localized (some providers/channels return {zh,en}). */
+export type LocalizedLabel = string | { zh: string; en: string }
+
+export interface ProviderMeta {
+ label: LocalizedLabel
+ models: string[]
+ api_base_key?: string | null
+ api_base_default?: string | null
+ api_base_placeholder?: string
+ api_key_field?: string | null
+ [k: string]: unknown
+}
+
+export interface ConfigData {
+ use_agent: boolean
+ title: string
+ model: string
+ bot_type: string
+ use_linkai: boolean
+ channel_type: string
+ agent_max_context_tokens: number
+ agent_max_context_turns: number
+ agent_max_steps: number
+ enable_thinking?: boolean
+ self_evolution_enabled?: boolean
+ api_bases: Record
+ api_keys: Record
+ providers: Record
+ web_password_masked?: string
+}
+
+// ============================================================
+// Models console (/api/models)
+// ============================================================
+
+// A model/voice entry can be a bare id or an annotated {value, hint} object.
+export interface ModelOption {
+ value: string
+ hint?: string
+}
+export type ModelEntry = string | ModelOption
+
+export interface ModelProvider {
+ id: string
+ label: LocalizedLabel
+ configured: boolean
+ is_custom: boolean
+ custom_id?: string
+ custom_name?: string
+ active?: boolean
+ api_key_field?: string | null
+ api_base_field?: string | null
+ api_key_masked?: string
+ api_base?: string
+ api_base_default?: string
+ api_base_placeholder?: string
+ models: ModelEntry[]
+}
+
+export type CapabilityKey = 'chat' | 'vision' | 'asr' | 'tts' | 'embedding' | 'image' | 'search'
+
+// Search providers are described as objects (unlike other capabilities which
+// list provider ids only).
+export interface SearchProviderMeta {
+ id: string
+ label: LocalizedLabel
+ configured: boolean
+ needs_dedicated_key: boolean
+ api_key_masked?: string
+}
+
+export interface CapabilityState {
+ editable?: boolean
+ current_provider?: string
+ current_model?: string
+ current_voice?: string
+ current_dim?: number | null
+ suggested_provider?: string
+ providers?: string[]
+ // provider_models entries are string | {value,hint}
+ provider_models?: Record
+ // tts only: voices keyed by provider; linkai keyed further by model id
+ provider_voices?: Record>
+ // vision/image
+ strategy?: string
+ user_specified_model?: string
+ fallback_provider?: string
+ fallback_model?: string
+ // tts
+ reply_mode?: 'off' | 'voice_if_voice' | 'always'
+ use_linkai?: boolean
+ // image
+ runtime_active?: boolean
+ note?: string
+ // search
+ fixed_provider?: string
+ configured_providers?: string[]
+ available?: boolean
+ [k: string]: unknown
+}
+
+export interface SearchCapabilityState {
+ editable?: boolean
+ providers: SearchProviderMeta[]
+ strategy?: 'auto' | 'fixed' | string
+ current_provider?: string
+ fixed_provider?: string
+ configured_providers?: string[]
+ available?: boolean
+}
+
+export interface ModelsData {
+ status?: string
+ providers: ModelProvider[]
+ capabilities: {
+ chat: CapabilityState
+ vision: CapabilityState
+ asr: CapabilityState
+ tts: CapabilityState
+ embedding: CapabilityState
+ image: CapabilityState
+ // search has a richer providers[] shape
+ search: SearchCapabilityState
+ }
+}
+
+export type ModelsAction =
+ | { action: 'set_provider'; provider_id: string; api_key?: string; api_base?: string }
+ | { action: 'delete_provider'; provider_id: string }
+ | { action: 'set_custom_provider'; name: string; id?: string; api_base: string; api_key?: string; model?: string; make_active?: boolean }
+ | { action: 'delete_custom_provider'; id: string }
+ | { action: 'set_active_custom_provider'; id: string }
+ | { action: 'set_capability'; capability: CapabilityKey; provider_id?: string; model?: string; voice?: string; strategy?: string; provider?: string }
+ | { action: 'set_voice_reply_mode'; mode: 'off' | 'voice_if_voice' | 'always' }
+ | { action: 'set_search_credential'; api_key: string }
+
+// ============================================================
+// Channels
+// ============================================================
+
+export interface ChannelField {
+ key: string
+ label: string
+ type: 'text' | 'secret' | 'number' | 'bool'
+ value?: string | number | boolean
+ default?: string | number | boolean
+}
+
+export interface ChannelInfo {
+ name: string
+ label: { zh: string; en: string }
+ icon: string
+ color: string
+ active: boolean
+ fields: ChannelField[]
+ login_status?: string
+}
+
+export type ChannelAction = 'save' | 'connect' | 'disconnect'
+
+// ============================================================
+// Tools / skills
+// ============================================================
+
+export interface ToolInfo {
+ name: string
+ description: string
+}
+
+export interface SkillInfo {
+ name: string
+ display_name?: string
+ description: string
+ source?: string
+ enabled: boolean
+ category?: string
+}
+
+// ============================================================
+// Memory
+// ============================================================
+
+export type MemoryCategory = 'memory' | 'dream' | 'evolution'
+
+export interface MemoryItem {
+ filename: string
+ type: string // global | daily | dream | evolution
+ size: number
+ updated_at: string
+}
+
+export interface MemoryPage {
+ list: MemoryItem[]
+ total: number
+ page: number
+ page_size: number
+}
+
+// ============================================================
+// Knowledge
+// ============================================================
+
+export interface KnowledgeFile {
+ name: string
+ title: string
+ size: number
+}
+
+// A directory node in the knowledge tree (recursive).
+export interface KnowledgeDir {
+ dir: string
+ files: KnowledgeFile[]
+ children: KnowledgeDir[]
+}
+
+export interface KnowledgeList {
+ root_files?: KnowledgeFile[]
+ tree: KnowledgeDir[]
+ stats: { pages: number; size: number }
+ enabled: boolean
+}
+
+export interface KnowledgeGraph {
+ nodes: Array<{ id: string; label: string; category?: string }>
+ links: Array<{ source: string; target: string }>
+}
+
+export type KnowledgeAction =
+ | { action: 'create_category'; payload: { path: string } }
+ | { action: 'rename_category'; payload: { path: string; new_path: string } }
+ | { action: 'delete_category'; payload: { path: string; confirm?: boolean } }
+ | { action: 'delete_documents'; payload: { paths: string[] } }
+ | { action: 'move_documents'; payload: { paths: string[]; target_category: string } }
+
+// ============================================================
+// Scheduler
+// ============================================================
+
+export interface TaskSchedule {
+ type: 'cron' | 'interval' | 'once'
+ expression?: string
+ seconds?: number
+ run_at?: string
+}
+
+export interface TaskAction {
+ type: 'send_message' | 'agent_task'
+ content?: string
+ task_description?: string
+ receiver?: string
+ receiver_name?: string
+ is_group?: boolean
+ channel_type?: string
+}
+
+export interface SchedulerTask {
+ id: string
+ name: string
+ enabled: boolean
+ created_at: string
+ updated_at: string
+ schedule: TaskSchedule
+ action: TaskAction
+ next_run_at?: string
+}
+
+// ============================================================
+// Logs
+// ============================================================
+
+export interface LogEvent {
+ type: 'init' | 'line' | 'error'
+ content?: string
+ message?: string
+}
+
+declare global {
+ interface Window {
+ electronAPI?: ElectronAPI
+ }
+}
diff --git a/desktop/tailwind.config.js b/desktop/tailwind.config.js
new file mode 100644
index 00000000..81789719
--- /dev/null
+++ b/desktop/tailwind.config.js
@@ -0,0 +1,80 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./src/renderer/**/*.{html,tsx,ts}'],
+ darkMode: 'class',
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: ['Inter', 'system-ui', '-apple-system', '"PingFang SC"', '"Hiragino Sans GB"', '"Microsoft YaHei"', 'sans-serif'],
+ mono: ['"JetBrains Mono"', '"Fira Code"', 'Consolas', 'monospace'],
+ },
+ colors: {
+ 'danger-soft': 'var(--danger-soft)',
+ 'danger-border': 'var(--danger-border)',
+ // Brand accent (kept for backward compat + explicit accent usage)
+ primary: {
+ 50: '#EDFDF3',
+ 100: '#D4FAE2',
+ 200: '#ABF4C7',
+ 300: '#74E9A4',
+ 400: '#4ABE6E',
+ 500: '#35A85B',
+ 600: '#228547',
+ 700: '#1C6B3B',
+ 800: '#1A5532',
+ 900: '#16462A',
+ },
+ // Semantic tokens — driven by CSS variables, theme-aware
+ accent: {
+ DEFAULT: 'var(--accent)',
+ hover: 'var(--accent-hover)',
+ active: 'var(--accent-active)',
+ soft: 'var(--accent-soft)',
+ contrast: 'var(--accent-contrast)',
+ },
+ base: 'var(--bg-base)',
+ surface: {
+ DEFAULT: 'var(--bg-surface)',
+ 2: 'var(--bg-surface-2)',
+ },
+ elevated: 'var(--bg-elevated)',
+ inset: 'var(--bg-inset)',
+ content: {
+ DEFAULT: 'var(--text-primary)',
+ secondary: 'var(--text-secondary)',
+ tertiary: 'var(--text-tertiary)',
+ disabled: 'var(--text-disabled)',
+ },
+ success: 'var(--success)',
+ warning: 'var(--warning)',
+ danger: 'var(--danger)',
+ info: 'var(--info)',
+ },
+ borderColor: {
+ DEFAULT: 'var(--border-default)',
+ default: 'var(--border-default)',
+ strong: 'var(--border-strong)',
+ subtle: 'var(--border-subtle)',
+ },
+ boxShadow: {
+ sm: 'var(--shadow-sm)',
+ md: 'var(--shadow-md)',
+ lg: 'var(--shadow-lg)',
+ },
+ borderRadius: {
+ card: '12px',
+ btn: '8px',
+ },
+ animation: {
+ 'pulse-dot': 'pulseDot 1.4s infinite ease-in-out both',
+ },
+ keyframes: {
+ pulseDot: {
+ '0%, 80%, 100%': { transform: 'scale(0.6)', opacity: '0.4' },
+ '40%': { transform: 'scale(1)', opacity: '1' },
+ },
+ },
+ },
+ },
+ plugins: [],
+}
diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json
new file mode 100644
index 00000000..d6d5250a
--- /dev/null
+++ b/desktop/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true
+ },
+ "include": ["src/renderer"]
+}
diff --git a/desktop/tsconfig.main.json b/desktop/tsconfig.main.json
new file mode 100644
index 00000000..9bc3504b
--- /dev/null
+++ b/desktop/tsconfig.main.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "commonjs",
+ "lib": ["ES2020"],
+ "outDir": "dist/main",
+ "rootDir": "src/main",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "declaration": false,
+ "sourceMap": true
+ },
+ "include": ["src/main/**/*"]
+}
diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts
new file mode 100644
index 00000000..942bd13c
--- /dev/null
+++ b/desktop/vite.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+
+export default defineConfig({
+ plugins: [react()],
+ root: path.resolve(__dirname, 'src/renderer'),
+ base: './',
+ publicDir: path.resolve(__dirname, '../channel/web/static'),
+ build: {
+ outDir: path.resolve(__dirname, 'dist/renderer'),
+ emptyOutDir: true,
+ },
+ server: {
+ port: 5173,
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src/renderer/src'),
+ },
+ },
+})
diff --git a/voice/dashscope/dashscope_voice.py b/voice/dashscope/dashscope_voice.py
index 746bb59a..e1f0770c 100644
--- a/voice/dashscope/dashscope_voice.py
+++ b/voice/dashscope/dashscope_voice.py
@@ -12,6 +12,7 @@ from dashscope import MultiModalConversation
from bridge.reply import Reply, ReplyType
from common.log import logger
+from common.tmp_dir import TmpDir
from config import conf
from voice import audio_convert
from voice.voice import Voice
@@ -121,8 +122,7 @@ class DashScopeVoice(Voice):
@staticmethod
def _download_audio(url: str) -> Optional[str]:
try:
- tmp_dir = os.path.join(os.getcwd(), "tmp")
- os.makedirs(tmp_dir, exist_ok=True)
+ tmp_dir = TmpDir().path()
ts = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
ext = os.path.splitext(url.split("?", 1)[0])[1].lower() or ".wav"
if ext not in (".mp3", ".wav", ".m4a", ".aac", ".opus"):