diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..b3ebfcb8b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Keep platform-specific scripts with safe line endings. +*.sh text eol=lf +*.command text eol=lf +*.ps1 text eol=crlf +*.bat text eol=crlf diff --git a/.github/workflows/desktop-ci.yml b/.github/workflows/desktop-ci.yml new file mode 100644 index 000000000..75a3db66f --- /dev/null +++ b/.github/workflows/desktop-ci.yml @@ -0,0 +1,157 @@ +name: Desktop Quality Gates + +on: + push: + branches: [main, desktop-react-rewrite] + paths: &desktop_paths + - "frontends/cost_tracker.py" + - "frontends/desktop_bridge.py" + - "frontends/tests/**" + - "frontends/desktop/**" + - ".github/workflows/desktop-ci.yml" + - ".github/workflows/desktop-e2e-nightly.yml" + - ".github/workflows/desktop-release-package.yml" + pull_request: + branches: [main, desktop-react-rewrite] + paths: *desktop_paths + workflow_dispatch: + +concurrency: + group: desktop-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + PYTHONUNBUFFERED: "1" + CARGO_TERM_COLOR: always + +jobs: + contracts: + name: TypeScript, Vitest, Python, Rust + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Install Tauri system dependencies + run: >- + sudo apt-get update && sudo apt-get install -y + libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xvfb + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontends/desktop/package-lock.json + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: frontends/desktop/e2e/requirements.txt + + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: frontends/desktop/src-tauri -> target + + - name: Install desktop dependencies + working-directory: frontends/desktop + run: npm ci + + - name: Install Python test runtime + run: python -m pip install -r frontends/desktop/e2e/requirements.txt + + - name: Type check + working-directory: frontends/desktop + run: npx tsc --noEmit && npm run test:e2e-types + + - name: Frontend and harness contracts + working-directory: frontends/desktop + run: npx vitest run + + - name: Python contracts + run: python -m pytest -q frontends/tests + + - name: Rust contracts (production feature set) + working-directory: frontends/desktop/src-tauri + run: cargo test + + - name: Rust contracts (E2E feature set) + working-directory: frontends/desktop/src-tauri + run: cargo test --features e2e + + - name: Production build excludes WDIO + working-directory: frontends/desktop + run: npm run test:e2e-isolation + + browser-e2e: + name: Browser E2E + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontends/desktop/package-lock.json + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: frontends/desktop/e2e/requirements.txt + - name: Install dependencies + working-directory: frontends/desktop + run: npm ci + - name: Install bridge runtime + run: python -m pip install -r frontends/desktop/e2e/requirements.txt + - name: Run deterministic browser journeys + working-directory: frontends/desktop + run: npm run e2e:browser + - name: Upload failure evidence + if: failure() + uses: actions/upload-artifact@v4 + with: + name: browser-e2e-${{ github.run_id }} + path: frontends/desktop/e2e-results + if-no-files-found: warn + + linux-tauri-smoke: + name: Linux Tauri smoke + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install Tauri system dependencies + run: >- + sudo apt-get update && sudo apt-get install -y + libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xvfb + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontends/desktop/package-lock.json + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: frontends/desktop/e2e/requirements.txt + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: frontends/desktop/src-tauri -> target + - name: Install dependencies + working-directory: frontends/desktop + run: npm ci + - name: Install bridge runtime + run: python -m pip install -r frontends/desktop/e2e/requirements.txt + - name: Build and drive native Tauri + working-directory: frontends/desktop + run: xvfb-run -a npm run e2e:desktop + - name: Upload failure evidence + if: failure() + uses: actions/upload-artifact@v4 + with: + name: linux-tauri-e2e-${{ github.run_id }} + path: frontends/desktop/e2e-results + if-no-files-found: warn diff --git a/.github/workflows/desktop-e2e-nightly.yml b/.github/workflows/desktop-e2e-nightly.yml new file mode 100644 index 000000000..879e692d4 --- /dev/null +++ b/.github/workflows/desktop-e2e-nightly.yml @@ -0,0 +1,89 @@ +name: Desktop E2E Nightly + +on: + schedule: + - cron: "30 18 * * *" + workflow_dispatch: + +concurrency: + group: desktop-nightly-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + native: + name: Native ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, windows-2025, macos-15] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Install Linux desktop dependencies + if: runner.os == 'Linux' + run: >- + sudo apt-get update && sudo apt-get install -y + libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xvfb + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontends/desktop/package-lock.json + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: frontends/desktop/e2e/requirements.txt + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: frontends/desktop/src-tauri -> target + - name: Install Node dependencies + working-directory: frontends/desktop + run: npm ci + - name: Install bridge runtime + run: python -m pip install -r frontends/desktop/e2e/requirements.txt + - name: Native full suite (Linux) + if: runner.os == 'Linux' + working-directory: frontends/desktop + run: xvfb-run -a npm run e2e:desktop:full + - name: Native full suite (Windows/macOS) + if: runner.os != 'Linux' + working-directory: frontends/desktop + run: npm run e2e:desktop:full + - name: Upload evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: native-${{ runner.os }}-${{ github.run_id }} + path: frontends/desktop/e2e-results + if-no-files-found: ignore + + canary: + name: Real-model protocol canary + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontends/desktop/package-lock.json + - name: Install dependencies + working-directory: frontends/desktop + run: npm ci + - name: Run canary when dedicated credentials exist + working-directory: frontends/desktop + env: + GA_E2E_CANARY_BASE: ${{ secrets.GA_E2E_CANARY_BASE }} + GA_E2E_CANARY_KEY: ${{ secrets.GA_E2E_CANARY_KEY }} + GA_E2E_CANARY_MODEL: ${{ secrets.GA_E2E_CANARY_MODEL }} + run: | + if [ -z "$GA_E2E_CANARY_KEY" ]; then + echo "Dedicated canary credentials are not configured; skipping." + exit 0 + fi + npm run e2e:canary diff --git a/.github/workflows/desktop-release-package.yml b/.github/workflows/desktop-release-package.yml new file mode 100644 index 000000000..8768271ce --- /dev/null +++ b/.github/workflows/desktop-release-package.yml @@ -0,0 +1,789 @@ +name: Build Desktop Portable Packages + +# Triggers: +# - workflow_dispatch -> build CI artifacts for testing (choose a target platform) +# - push tag desktop-portable-* -> build all three platforms and publish them +# together into ONE GitHub Release (tag = the pushed tag) +# Plain pushes to branches do NOT build (avoids 3 heavy Rust builds per commit). +"on": + push: + tags: + - "desktop-portable-*" + workflow_dispatch: + inputs: + target: + description: "Which platform(s) to build (artifacts only; release happens on tag push)" + required: false + default: "all" + type: choice + options: + - "all" + - "windows" + - "linux" + - "macos" + +permissions: + contents: write + +jobs: + # ---------------------------------------------------------------------------- + build-windows: + name: Windows portable + if: ${{ startsWith(github.ref, 'refs/tags/') || github.event.inputs.target == 'all' || github.event.inputs.target == 'windows' }} + runs-on: windows-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontends/desktop/package-lock.json + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + with: + workspaces: frontends/desktop/src-tauri -> target + + - name: Install desktop dependencies + working-directory: frontends/desktop + run: npm ci + + - name: Build Windows desktop exe + working-directory: frontends/desktop + run: npm run tauri build -- --bundles nsis + + - name: Assemble self-contained portable bundle + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + EXE_SRC="$(find frontends/desktop/src-tauri/target/release -maxdepth 1 -type f -name 'ga-desktop.exe' | head -n 1)" + [[ -n "$EXE_SRC" ]] || { echo "ga-desktop.exe not found" >&2; exit 1; } + + PKG="artifacts/windows/GenericAgent-Desktop-Windows-Portable" + RUNTIME="$PKG/runtime" + mkdir -p "$RUNTIME" + + # exe at the top level, named GenericAgent (typical portable form) + cp "$EXE_SRC" "$PKG/GenericAgent.exe" + cp frontends/desktop/packaging/scripts/windows/install_windows.ps1 "$RUNTIME/install_windows.ps1" + # Uninstaller: double-click entry at top level, worker script under runtime/. + cp frontends/desktop/packaging/scripts/windows/uninstall.bat "$PKG/uninstall.bat" + cp frontends/desktop/packaging/scripts/windows/uninstall_windows.ps1 "$RUNTIME/uninstall_windows.ps1" + + # Embedded Python (python-build-standalone, windows x86_64, 3.12 install_only). + # browser_download_url is URL-encoded ('+' -> '%2B'); match loosely with '.*'. + PBS_URL="$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest \ + | grep -o '"browser_download_url": *"[^"]*"' \ + | grep 'cpython-3\.12\.[0-9].*x86_64-pc-windows-msvc-install_only\.tar\.gz' \ + | head -1 | sed -E 's/.*"(https[^"]+)"/\1/')" + [[ -n "$PBS_URL" ]] || { echo "Could not resolve python-build-standalone download URL" >&2; exit 1; } + echo "Python build-standalone: $PBS_URL" + curl -fsSL -o /tmp/pbs.tar.gz "$PBS_URL" + tar -xzf /tmp/pbs.tar.gz -C "$RUNTIME" # extracts a 'python/' dir (python.exe at top) + PY="$RUNTIME/python/python.exe" + "$PY" --version + + # App-local UCRT: python-build-standalone links the Universal CRT dynamically + # and does NOT bundle it. Win10/11 ship UCRT in-box, but stripped/older images + # may not -> python.exe fails to load (missing api-ms-win-crt-*.dll). Copy the + # redistributable UCRT DLLs next to python.exe (Microsoft-supported app-local + # deployment). Pure SDK source, no System32 fallback, hard-fail if absent so we + # never ship a bundle from an unexpected source. + UCRT_SRC="$(ls -d "/c/Program Files (x86)/Windows Kits/10/Redist/"*/ucrt/DLLs/x64 2>/dev/null | sort -V | tail -1)" + [[ -n "$UCRT_SRC" ]] || { echo "Windows SDK UCRT redist dir not found on runner" >&2; exit 1; } + echo "UCRT source: $UCRT_SRC" + cp "$UCRT_SRC"/api-ms-win-crt-*.dll "$RUNTIME/python/" + cp "$UCRT_SRC"/ucrtbase.dll "$RUNTIME/python/" + test -f "$RUNTIME/python/api-ms-win-crt-runtime-l1-1-0.dll" \ + || { echo "UCRT DLLs missing after copy" >&2; exit 1; } + + # Offline wheels (native win_amd64 wheels for the embedded python) + mkdir -p "$RUNTIME/wheels" + "$PY" -m pip download --dest "$RUNTIME/wheels" \ + "requests>=2.28" "beautifulsoup4>=4.12" "bottle>=0.12" "simple-websocket-server>=0.4" "aiohttp>=3.9" psutil \ + fastapi uvicorn websockets pydantic setuptools wheel + + # Runtime source (project root minus heavy/dev/build dirs) + mkdir -p "$RUNTIME/app" + tar \ + --exclude='./.git' --exclude='./.github' \ + --exclude='./frontends/desktop/src-tauri' \ + --exclude='./frontends/desktop/node_modules' \ + --exclude='./frontends/desktop/packaging' --exclude='./docs' \ + --exclude='./assets/demo' --exclude='./assets/images' \ + --exclude='./assets/GenericAgent_Technical_Report.pdf' \ + --exclude='./artifacts' \ + --exclude='*/node_modules' --exclude='*/target' \ + --exclude='*/.venv' --exclude='./.venv' \ + --exclude='*/__pycache__' --exclude='*.pyc' \ + -cf - . | tar -xf - -C "$RUNTIME/app" + test -f "$RUNTIME/app/agentmain.py" + test -f "$RUNTIME/app/frontends/desktop_bridge.py" + test -f "$RUNTIME/app/frontends/desktop/static/index.html" + + # Drop python debug symbols (.pdb) to slim the package (~80MB) + find "$RUNTIME/python" -name '*.pdb' -delete 2>/dev/null || true + + cat > "$PKG/readme.txt" <<'EOF' + ================ 中文 ================ + GenericAgent Desktop — Windows 便携版(自包含) + + 无需安装 Python、无需联网装依赖、无需源码仓库——全部已内置。 + + 前置条件 + - Windows 10/11 x64 + - Microsoft Edge WebView2 运行时(Win11 一般自带;缺失时首次运行会提示安装) + + 使用 + 1. 解压到任意目录(路径建议不含特殊字符)。 + 2. 双击 GenericAgent.exe。 + 3. 首次启动会自动离线准备运行环境(建虚拟环境、装依赖),界面显示进度,完成后进入主界面。 + 4. 之后启动直接秒进。 + + 说明 + - 想真正对话,仍需在程序里配置模型 / API Key。 + - 首次准备完成后不要移动本文件夹;若已移动,删除 runtime\app\.venv 后重新启动即可重建。 + - runtime\ 是内置运行环境与源码,正常使用无需改动。 + + ================ English ================ + GenericAgent Desktop — Windows Portable (self-contained) + + No Python install, no internet for dependencies, no source checkout — everything is bundled. + + Requirements + - Windows 10/11 x64 + - Microsoft Edge WebView2 runtime (usually preinstalled on Win11; you are prompted if missing) + + Usage + 1. Extract anywhere (a path without special characters is recommended). + 2. Double-click GenericAgent.exe. + 3. The first launch prepares the runtime offline (creates a venv, installs deps) with a + progress UI, then opens the main window. + 4. Subsequent launches start instantly. + + Notes + - To actually chat, configure a model / API key inside the app. + - Do not move this folder after the first setup; if you did, delete runtime\app\.venv and relaunch to rebuild. + - runtime\ holds the bundled runtime and source; no need to touch it. + EOF + sed -i 's/^ //' "$PKG/readme.txt" + + echo "Package tree (top levels):" + find "$PKG" -maxdepth 2 -printf '%y %p\n' | sort | head -40 + + - name: Zip portable package + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path artifacts/windows/out | Out-Null + Compress-Archive -Path artifacts/windows/GenericAgent-Desktop-Windows-Portable ` + -DestinationPath artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip -Force + $h = Get-FileHash artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip -Algorithm SHA256 + $shaPath = [System.IO.Path]::GetFullPath( + "artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip.sha256" + ) + $shaLine = "{0} GenericAgent-Desktop-Windows-Portable.zip`n" -f $h.Hash.ToLowerInvariant() + [System.IO.File]::WriteAllText($shaPath, $shaLine, [System.Text.UTF8Encoding]::new($false)) + if ([System.IO.File]::ReadAllBytes($shaPath) -contains 13) { + throw "Windows checksum sidecar must use LF line endings" + } + Get-ChildItem artifacts/windows/out | Format-Table Name, Length + + - name: Exercise Windows portable candidate + if: ${{ github.event_name == 'workflow_dispatch' }} + shell: pwsh + run: | + & .\frontends\desktop\e2e\windows\Invoke-WindowsUserJourney.ps1 ` + -RunId "${{ github.run_id }}" ` + -ExpectedCommit "${{ github.sha }}" ` + -PackageZip "artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip" ` + -WorkDir "${{ runner.temp }}\ga-windows-portable-e2e" ` + -Mode Full ` + -KeepWorkDir + + - name: Upload Windows journey evidence + if: ${{ always() && github.event_name == 'workflow_dispatch' }} + uses: actions/upload-artifact@v7 + with: + name: GenericAgent-Desktop-Windows-Journey-${{ github.run_number }} + path: ${{ runner.temp }}/ga-windows-portable-e2e/report + if-no-files-found: warn + + - name: Upload workflow artifact + uses: actions/upload-artifact@v7 + with: + name: GenericAgent-Desktop-Windows-Portable-${{ github.run_number }} + path: | + artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip + artifacts/windows/out/GenericAgent-Desktop-Windows-Portable.zip.sha256 + if-no-files-found: error + + # ---------------------------------------------------------------------------- + build-linux: + name: Linux portable + if: ${{ startsWith(github.ref, 'refs/tags/') || github.event.inputs.target == 'all' || github.event.inputs.target == 'linux' }} + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontends/desktop/package-lock.json + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + with: + workspaces: frontends/desktop/src-tauri -> target + + - name: Install Tauri Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf + + - name: Install desktop dependencies + working-directory: frontends/desktop + run: npm ci + + - name: Build Linux AppImage + working-directory: frontends/desktop + run: npm run tauri build -- --bundles appimage + + - name: Assemble self-contained portable bundle + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + APPIMAGE_SRC="$(find frontends/desktop/src-tauri/target/release/bundle/appimage -maxdepth 1 -type f -name '*.AppImage' | head -n 1)" + [[ -n "$APPIMAGE_SRC" ]] || { echo "No AppImage found" >&2; exit 1; } + + PKG="artifacts/linux/GenericAgent-Desktop-Linux-Portable" + RUNTIME="$PKG/runtime" + mkdir -p "$RUNTIME" + + # Stable internal name so the generated .desktop Exec= never changes on upgrades. + cp "$APPIMAGE_SRC" "$PKG/GenericAgent.AppImage" + chmod +x "$PKG/GenericAgent.AppImage" + # Icon for the generated .desktop launcher (Icon= needs a real image file on Linux). + cp frontends/desktop/src-tauri/icons/icon.png "$PKG/GenericAgent.png" + cp frontends/desktop/packaging/scripts/linux/install_linux.sh "$RUNTIME/install_linux.sh" + chmod +x "$RUNTIME/install_linux.sh" + # Uninstaller: top-level entry next to the AppImage. + cp frontends/desktop/packaging/scripts/linux/uninstall.sh "$PKG/uninstall.sh" + chmod +x "$PKG/uninstall.sh" + + # Embedded Python (python-build-standalone, linux x86_64, 3.12 install_only). + PBS_URL="$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest \ + | grep -o '"browser_download_url": *"[^"]*"' \ + | grep 'cpython-3\.12\.[0-9].*x86_64-unknown-linux-gnu-install_only\.tar\.gz' \ + | head -1 | sed -E 's/.*"(https[^"]+)"/\1/')" + [[ -n "$PBS_URL" ]] || { echo "Could not resolve python-build-standalone download URL" >&2; exit 1; } + echo "Python build-standalone: $PBS_URL" + curl -fsSL -o /tmp/pbs.tar.gz "$PBS_URL" + tar -xzf /tmp/pbs.tar.gz -C "$RUNTIME" # extracts a 'python/' dir + PY="$RUNTIME/python/bin/python3" + "$PY" --version + + # Offline wheels (native linux wheels for the embedded python) + mkdir -p "$RUNTIME/wheels" + "$PY" -m pip download --dest "$RUNTIME/wheels" \ + "requests>=2.28" "beautifulsoup4>=4.12" "bottle>=0.12" "simple-websocket-server>=0.4" "aiohttp>=3.9" psutil \ + fastapi uvicorn websockets pydantic setuptools wheel + + # Runtime source (project root minus heavy/dev/build dirs) + mkdir -p "$RUNTIME/app" + tar \ + --exclude='./.git' --exclude='./.github' \ + --exclude='./frontends/desktop/src-tauri' \ + --exclude='./frontends/desktop/node_modules' \ + --exclude='./frontends/desktop/packaging' --exclude='./docs' \ + --exclude='./assets/demo' --exclude='./assets/images' \ + --exclude='./assets/GenericAgent_Technical_Report.pdf' \ + --exclude='./artifacts' \ + --exclude='*/node_modules' --exclude='*/target' \ + --exclude='*/.venv' --exclude='./.venv' \ + --exclude='*/__pycache__' --exclude='*.pyc' \ + -cf - . | tar -xf - -C "$RUNTIME/app" + test -f "$RUNTIME/app/agentmain.py" + test -f "$RUNTIME/app/frontends/desktop_bridge.py" + test -f "$RUNTIME/app/frontends/desktop/static/index.html" + + # Drop python debug/cache bits to slim the package + find "$RUNTIME/python" -name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true + + cat > "$PKG/readme.txt" <<'EOF' + ================ 中文 ================ + GenericAgent Desktop — Linux 便携版(自包含) + + 无需安装 Python、无需联网装依赖、无需源码仓库——全部已内置。 + + 前置条件 + - Linux x86_64(glibc,常见发行版即可) + - 桌面环境 + FUSE(运行 AppImage 所需;多数发行版自带) + + 使用 + 1. 解压到任意目录(路径建议不含特殊字符)。 + 2. 给 AppImage 执行权限并运行: + chmod +x GenericAgent.AppImage + ./GenericAgent.AppImage + 3. 首次启动会自动离线准备运行环境(建虚拟环境、装依赖),界面显示进度,完成后进入主界面。 + 4. 之后启动直接秒进。 + + 说明 + - 想真正对话,仍需在程序里配置模型 / API Key。 + - 首次准备完成后不要移动本文件夹;若已移动,删除 runtime/app/.venv 后重新启动即可重建。 + - runtime/ 是内置运行环境与源码,正常使用无需改动。 + + ================ English ================ + GenericAgent Desktop — Linux Portable (self-contained) + + No Python install, no internet for dependencies, no source checkout — everything is bundled. + + Requirements + - Linux x86_64 (glibc; common distributions work) + - A desktop environment + FUSE (required to run the AppImage; preinstalled on most distros) + + Usage + 1. Extract anywhere (a path without special characters is recommended). + 2. Make the AppImage executable and run it: + chmod +x GenericAgent.AppImage + ./GenericAgent.AppImage + 3. The first launch prepares the runtime offline (creates a venv, installs deps) with a + progress UI, then opens the main window. + 4. Subsequent launches start instantly. + + Notes + - To actually chat, configure a model / API key inside the app. + - Do not move this folder after the first setup; if you did, delete runtime/app/.venv and relaunch to rebuild. + - runtime/ holds the bundled runtime and source; no need to touch it. + EOF + sed -i 's/^ //' "$PKG/readme.txt" + + mkdir -p artifacts/linux/out + tar -C artifacts/linux -czf artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz GenericAgent-Desktop-Linux-Portable + ( cd artifacts/linux/out && sha256sum GenericAgent-Desktop-Linux-Portable.tar.gz > GenericAgent-Desktop-Linux-Portable.tar.gz.sha256 ) + + tar -tzf artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz > /tmp/pkglist.txt + echo "Package contents (first 40 of $(wc -l < /tmp/pkglist.txt) entries):" + head -40 /tmp/pkglist.txt + ls -lh artifacts/linux/out + + - name: Upload workflow artifact + uses: actions/upload-artifact@v7 + with: + name: GenericAgent-Desktop-Linux-Portable-${{ github.run_number }} + path: | + artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz + artifacts/linux/out/GenericAgent-Desktop-Linux-Portable.tar.gz.sha256 + if-no-files-found: error + + # ---------------------------------------------------------------------------- + build-macos: + name: macOS DMG + if: ${{ startsWith(github.ref, 'refs/tags/') || github.event.inputs.target == 'all' || github.event.inputs.target == 'macos' }} + runs-on: macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + cache-dependency-path: frontends/desktop/package-lock.json + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + with: + workspaces: frontends/desktop/src-tauri -> target + + - name: Install desktop dependencies + working-directory: frontends/desktop + run: npm ci + + - name: Install Python packaging tools + run: python3 -m pip install --break-system-packages ds_store + + - name: Build macOS .app + working-directory: frontends/desktop + run: npm run tauri build -- --bundles app + + - name: Assemble self-contained portable bundle and DMG app + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + APP_SRC="$(find frontends/desktop/src-tauri/target/release/bundle/macos -maxdepth 1 -name '*.app' -type d | head -n 1)" + [[ -n "$APP_SRC" ]] || { echo "No .app found" >&2; exit 1; } + + # Build one runtime/ tree, then reuse it for both macOS deliverables: + # 1) Portable zip: GenericAgent.app + runtime/ + helper scripts. + # 2) Standard DMG: GenericAgent.app with runtime embedded in Contents/Resources/runtime. + RUNTIME_SRC="artifacts/macos/runtime-src" + mkdir -p "$RUNTIME_SRC" + cp frontends/desktop/packaging/scripts/macos/install_macos.sh "$RUNTIME_SRC/install_macos.sh" + chmod +x "$RUNTIME_SRC/install_macos.sh" + + # Embedded Python (python-build-standalone, macOS, 3.12 install_only; aarch64 preferred). + PBS_URL="$(python3 - <<'PY' + import json, os, urllib.request + api='https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest' + req=urllib.request.Request(api) + token=os.environ.get('GH_TOKEN') + if token: + req.add_header('Authorization', 'Bearer ' + token) + data=json.load(urllib.request.urlopen(req, timeout=60)) + assets=data.get('assets', []) + for arch in ('aarch64-apple-darwin', 'x86_64-apple-darwin'): + for a in assets: + name=a.get('name','') + if 'cpython-3.12' in name and arch in name and name.endswith('install_only.tar.gz') and 'stripped' not in name: + print(a['browser_download_url']); raise SystemExit + raise SystemExit('No suitable python-build-standalone macOS asset found') + PY + )" + echo "Python build-standalone: $PBS_URL" + curl -L --fail --retry 3 -o /tmp/pbs-macos.tar.gz "$PBS_URL" + tar -xzf /tmp/pbs-macos.tar.gz -C "$RUNTIME_SRC" # extracts a 'python/' dir + PY="$RUNTIME_SRC/python/bin/python3" + "$PY" --version + + # Offline wheels + mkdir -p "$RUNTIME_SRC/wheels" + "$PY" -m pip download --dest "$RUNTIME_SRC/wheels" \ + "requests>=2.28" "beautifulsoup4>=4.12" "bottle>=0.12" "simple-websocket-server>=0.4" "aiohttp>=3.9" psutil \ + fastapi uvicorn websockets pydantic setuptools wheel + + # Runtime source (project root minus heavy/dev/build dirs) — bsdtar exclude syntax. + mkdir -p "$RUNTIME_SRC/app" + tar \ + --exclude='.git' --exclude='.github' \ + --exclude='frontends/desktop/src-tauri' \ + --exclude='frontends/desktop/node_modules' \ + --exclude='frontends/desktop/packaging' --exclude='docs' \ + --exclude='assets/demo' --exclude='assets/images' \ + --exclude='assets/GenericAgent_Technical_Report.pdf' \ + --exclude='artifacts' \ + --exclude='*/node_modules' --exclude='*/target' \ + --exclude='*/.venv' --exclude='.venv' \ + --exclude='*/__pycache__' --exclude='*.pyc' \ + -cf - . | tar -xf - -C "$RUNTIME_SRC/app" + test -f "$RUNTIME_SRC/app/agentmain.py" + test -f "$RUNTIME_SRC/app/frontends/desktop_bridge.py" + test -f "$RUNTIME_SRC/app/frontends/desktop/static/index.html" + + PORTABLE="artifacts/macos/GenericAgent-Desktop-macOS-Portable" + DMG_STAGE="artifacts/macos/dmg-stage" + DMG_APP="$DMG_STAGE/GenericAgent.app" + mkdir -p "$PORTABLE" "$DMG_STAGE" + + # Portable zip: keep the existing release shape and helper scripts. + ditto "$APP_SRC" "$PORTABLE/GenericAgent.app" + ditto "$RUNTIME_SRC" "$PORTABLE/runtime" + cp frontends/desktop/packaging/scripts/macos/uninstall.command "$PORTABLE/uninstall.command" + chmod +x "$PORTABLE/uninstall.command" + cat > "$PORTABLE/Start GenericAgent.command" <<'EOF' + #!/bin/bash + DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + xattr -cr "$DIR/GenericAgent.app" 2>/dev/null || true + open "$DIR/GenericAgent.app" + EOF + chmod +x "$PORTABLE/Start GenericAgent.command" + + cat > "$PORTABLE/readme.txt" <<'EOF' + ================ 中文 ================ + GenericAgent Desktop — macOS 便携版(自包含) + + 无需安装 Python、无需联网装依赖、无需源码仓库——全部已内置。 + + 使用 + 推荐:双击 “Start GenericAgent.command” 启动。也可直接双击 GenericAgent.app。 + 请保持目录结构不变:GenericAgent.app 与 runtime/ 必须在同一目录下。 + + 卸载 + 双击 uninstall.command,或退出程序后删除整个便携文件夹。 + + ================ English ================ + GenericAgent Desktop — macOS Portable (self-contained) + + No Python install, no internet for dependencies, no source checkout — everything is bundled. + + Usage + Recommended: double-click "Start GenericAgent.command". You can also double-click GenericAgent.app. + Keep the folder layout intact: GenericAgent.app and runtime/ must stay in the same directory. + + Uninstall + Double-click uninstall.command, or quit the app and delete the whole portable folder. + EOF + sed -i '' 's/^ //' "$PORTABLE/readme.txt" + + codesign --force --deep --sign - "$PORTABLE/GenericAgent.app" + codesign --verify --deep --strict "$PORTABLE/GenericAgent.app" || true + + # Standard DMG: GenericAgent.app + Applications alias + first-launch helper/guide. + ditto "$APP_SRC" "$DMG_APP" + mkdir -p "$DMG_APP/Contents/Resources" + ditto "$RUNTIME_SRC" "$DMG_APP/Contents/Resources/runtime" + codesign --force --deep --sign - "$DMG_APP" + codesign --verify --deep --strict "$DMG_APP" || true + ln -s /Applications "$DMG_STAGE/Applications" + + cat > "$DMG_STAGE/open_anyway.command" <<'EOF' + #!/bin/bash + set -e + DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + TARGET="/Applications/GenericAgent.app" + if [[ ! -d "$TARGET" ]]; then + TARGET="$DIR/GenericAgent.app" + fi + xattr -cr "$TARGET" 2>/dev/null || true + osascript -e 'display dialog "GenericAgent 已处理完成。请将 GenericAgent.app 拖入 Applications 后,从 Applications 中启动;如果已经安装,可以直接重新打开。" buttons {"OK"} default button "OK"' >/dev/null 2>&1 || true + EOF + chmod +x "$DMG_STAGE/open_anyway.command" + + cat > "$DMG_STAGE/readme.txt" <<'EOF' + ================ 中文 ================ + GenericAgent Desktop — macOS DMG 安装版 + + 安装 + 1. 双击打开 GenericAgent-Desktop-macOS.dmg。 + 2. 将 GenericAgent.app 拖入 Applications。 + 3. 打开“应用程序 / Applications”,双击 GenericAgent.app 启动。 + + 若出现“无法验证开发者”“Apple 无法检查其是否包含恶意软件”或无法打开: + 1. 回到 DMG 窗口。 + 2. 双击“open_anyway.command”。 + 3. 再从 Applications 中打开 GenericAgent.app。 + 也可以右键 GenericAgent.app,选择“打开”。 + + 卸载 + 先退出 GenericAgent,再从 Applications 删除 GenericAgent.app。 + + ================ English ================ + GenericAgent Desktop — macOS DMG installer + + Installation + 1. Open GenericAgent-Desktop-macOS.dmg. + 2. Drag GenericAgent.app to Applications. + 3. Open Applications and launch GenericAgent.app. + + If macOS says the developer cannot be verified, Apple cannot check it for malicious software, or the app cannot be opened: + 1. Go back to the DMG window. + 2. Double-click "open_anyway.command". + 3. Launch GenericAgent.app from Applications again. + You can also right-click GenericAgent.app and choose Open. + + Uninstall + Quit GenericAgent, then delete GenericAgent.app from Applications. + EOF + sed -i '' 's/^ //' "$DMG_STAGE/readme.txt" + + mkdir -p artifacts/macos/out + hdiutil create \ + -volname "GenericAgent Desktop" \ + -srcfolder "$DMG_STAGE" \ + -ov \ + -format UDZO \ + "artifacts/macos/out/GenericAgent-Desktop-macOS.dmg" + + # Hide ghost files (.VolumeIcon.icns, .background, .fseventsd) from Finder + bash frontends/desktop/scripts/post-dmg.sh "artifacts/macos/out/GenericAgent-Desktop-macOS.dmg" + + ( + cd artifacts/macos/out + shasum -a 256 "GenericAgent-Desktop-macOS.dmg" \ + > "GenericAgent-Desktop-macOS.dmg.sha256" + ) + + echo "DMG stage tree:" + find "$DMG_STAGE" -maxdepth 2 -print | sort + + - name: Upload workflow artifact + uses: actions/upload-artifact@v7 + with: + name: GenericAgent-Desktop-macOS-DMG-${{ github.run_number }} + path: artifacts/macos/out/* + if-no-files-found: error + + # A tag may publish only after every platform has produced and uploaded its + # complete pair of package + checksum files. Keeping publication in one job + # prevents concurrent platform jobs from exposing a partial public release. + publish-release: + name: Publish unified Release + if: ${{ startsWith(github.ref, 'refs/tags/') }} + needs: + - build-windows + - build-linux + - build-macos + runs-on: ubuntu-24.04 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + steps: + - name: Checkout release tag + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Download Windows package + uses: actions/download-artifact@v8 + with: + name: GenericAgent-Desktop-Windows-Portable-${{ github.run_number }} + path: release-assets/windows + + - name: Download Linux package + uses: actions/download-artifact@v8 + with: + name: GenericAgent-Desktop-Linux-Portable-${{ github.run_number }} + path: release-assets/linux + + - name: Download macOS package + uses: actions/download-artifact@v8 + with: + name: GenericAgent-Desktop-macOS-DMG-${{ github.run_number }} + path: release-assets/macos + + - name: Verify all release assets + shell: bash + run: | + set -euo pipefail + + expected=( + "release-assets/windows/GenericAgent-Desktop-Windows-Portable.zip" + "release-assets/windows/GenericAgent-Desktop-Windows-Portable.zip.sha256" + "release-assets/linux/GenericAgent-Desktop-Linux-Portable.tar.gz" + "release-assets/linux/GenericAgent-Desktop-Linux-Portable.tar.gz.sha256" + "release-assets/macos/GenericAgent-Desktop-macOS.dmg" + "release-assets/macos/GenericAgent-Desktop-macOS.dmg.sha256" + ) + for asset in "${expected[@]}"; do + test -s "$asset" || { echo "Missing or empty release asset: $asset" >&2; exit 1; } + done + + test "$(find release-assets -type f | wc -l)" -eq 6 || { + echo "Expected exactly six release files" >&2 + find release-assets -type f -print | sort >&2 + exit 1 + } + + (cd release-assets/windows && sha256sum -c GenericAgent-Desktop-Windows-Portable.zip.sha256) + (cd release-assets/linux && sha256sum -c GenericAgent-Desktop-Linux-Portable.tar.gz.sha256) + (cd release-assets/macos && sha256sum -c GenericAgent-Desktop-macOS.dmg.sha256) + + - name: Prepare release notes + shell: bash + env: + TAG_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + : > release-notes.md + + if [[ "$TAG_NAME" == "desktop-portable-v0.2.1" ]]; then + cat >> release-notes.md <<'EOF' + ## 本次更新 / What's new + + - Token 用量在每次模型调用后立即持久化,桌面桥接进程崩溃或重启后历史记录仍可恢复。 + - 空回复现在会显示本地化提示,不再留下难以理解的空白消息。 + - 加强桌面恢复能力和跨平台自动化验证,覆盖损坏数据、进程重启和便携包用户旅程。 + + - Token usage is persisted after every model call and survives desktop bridge crashes and restarts. + - Empty assistant turns now render a localized explanatory message instead of a blank response. + - Desktop recovery and cross-platform automation now cover corrupted data, process restarts, and portable-package journeys. + + EOF + fi + + cat >> release-notes.md <<'EOF' + ## 下载与校验 / Download and verification + + 每个平台均提供安装包及对应的 `.sha256` 文件。下载两者后可校验文件完整性: + + ```bash + sha256sum -c .sha256 + ``` + + Each platform includes the package and its matching `.sha256` file. Download both and verify the package before installation. + + - Windows: 解压 ZIP,运行 `GenericAgent.exe`。 + - Linux: 解压 `tar.gz`,为 AppImage 添加执行权限后启动。 + - macOS: 打开 DMG,将 `GenericAgent.app` 拖入 Applications;若 Gatekeeper 阻止启动,请使用 DMG 内的 `open_anyway.command`。 + + 所有包均内置 Python、运行时依赖、桌面桥接服务和静态前端资源,无需另行安装 Python。 + EOF + + - name: Create draft, upload atomically, then publish + shell: bash + env: + TAG_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + + target_sha="$(git rev-list -n 1 "${TAG_NAME}^{commit}")" + test -n "$target_sha" + + if gh release view "$TAG_NAME" --json isDraft >/tmp/release.json 2>/dev/null; then + test "$(jq -r '.isDraft' /tmp/release.json)" = "true" || { + echo "Refusing to modify an already-public release: $TAG_NAME" >&2 + exit 1 + } + gh release edit "$TAG_NAME" \ + --draft \ + --target "$target_sha" \ + --title "GenericAgent Desktop Portable ${TAG_NAME#desktop-portable-}" \ + --notes-file release-notes.md + else + gh release create "$TAG_NAME" \ + --draft \ + --verify-tag \ + --target "$target_sha" \ + --title "GenericAgent Desktop Portable ${TAG_NAME#desktop-portable-}" \ + --notes-file release-notes.md + fi + + gh release upload "$TAG_NAME" \ + release-assets/windows/GenericAgent-Desktop-Windows-Portable.zip \ + release-assets/windows/GenericAgent-Desktop-Windows-Portable.zip.sha256 \ + release-assets/linux/GenericAgent-Desktop-Linux-Portable.tar.gz \ + release-assets/linux/GenericAgent-Desktop-Linux-Portable.tar.gz.sha256 \ + release-assets/macos/GenericAgent-Desktop-macOS.dmg \ + release-assets/macos/GenericAgent-Desktop-macOS.dmg.sha256 \ + --clobber + + cat > /tmp/expected-assets.txt <<'EOF' + GenericAgent-Desktop-Linux-Portable.tar.gz + GenericAgent-Desktop-Linux-Portable.tar.gz.sha256 + GenericAgent-Desktop-Windows-Portable.zip + GenericAgent-Desktop-Windows-Portable.zip.sha256 + GenericAgent-Desktop-macOS.dmg + GenericAgent-Desktop-macOS.dmg.sha256 + EOF + gh release view "$TAG_NAME" --json assets --jq '.assets[].name' \ + | sort > /tmp/actual-assets.txt + diff -u /tmp/expected-assets.txt /tmp/actual-assets.txt + + gh release edit "$TAG_NAME" --draft=false --latest diff --git a/.gitignore b/.gitignore index c8ca6bf0d..994f929f5 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..fb9f4116e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,44 @@ +# Contributing to GenericAgent + +## Why This File Is Short + +GenericAgent's core is ~3K lines. Every file in this repo will be read by AI agents — potentially thousands of times. Extra words cost real tokens and push useful context out of the window, increasing hallucinations. This document practices what it preaches: **say only what matters.** + +## Before You Contribute + +1. **Read the codebase first.** It's small enough to read in one sitting. Understand the philosophy before proposing changes. +2. **Open an Issue first** for anything non-trivial. Discuss before coding. + +## Code Standards + +All PRs go through a strict automated code review skill. Key expectations: + +- **Self-documenting code, minimal comments.** If code needs a paragraph to explain, rewrite it. +- **Compact and visually uniform.** Fewer lines, consistent line lengths, no fluff. +- **Small change radius.** Changing A shouldn't ripple through B, C, D. +- **More features → less code.** Good abstractions make the codebase shrink, not grow. +- **Let it crash by failure radius.** Critical errors fail loud; trivial ones pass silently. No blanket try-catch. + +> ⚠️ This review is deliberately strict — most AI-generated code (e.g. Claude Code output) will not pass as-is. Read the full principles before submitting. + +## Skill Contributions + +GenericAgent evolves through skills. Not all skills belong in the core repo: + +| Type | Where it goes | Example | +|---|---|---| +| **Fundamental / universal** | Core repo (`memory/`) | File search, clipboard, basic web ops | +| **Domain-specific / niche** | Skill Marketplace *(coming soon)* | Stock screening, food delivery, specific API integrations | + +If your skill only makes sense for a specific workflow, it's a marketplace candidate, not a core PR. + +## PR Checklist + +- [ ] Issue linked or context explained in ≤3 sentences +- [ ] Code passes the [review principles] self-check: + 1. Can I safely modify this locally without reading the whole codebase? + 2. Is there a clear core abstraction — new features add implementations, not modify old logic? + 3. Are change points converging at boundaries, not scattered everywhere? + 4. On failure, can I quickly locate the responsible module? +- [ ] Net line count: ideally negative or zero for refactors +- [ ] No unnecessary dependencies added diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..641d051c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 lsdefine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 8c7e2ff30..0697dea49 100644 --- a/README.md +++ b/README.md @@ -1,281 +1,818 @@ -# GenericAgent — 3,300 Lines to Full OS Autonomy +
-[English](#english) | [中文](#chinese) +GenericAgent Banner - +# GenericAgent -A minimalist autonomous agent framework that gives any LLM physical-level control over your PC — browser, terminal, file system, keyboard, mouse, screen vision, and mobile devices — in ~3,300 lines of Python. +**A Minimal, Self-Evolving Autonomous Agent Framework** -No Electron. No Docker. No Mac Mini. No 500K-line codebase. No paid installation service. +*~3K lines of seed code · 9 atomic tools · ~100-line Agent Loop* -## See It in Action +

- - - - - -

"Order me a milk tea" — navigates a delivery app, picks items, and checks out.

"Find GEM stocks with EXPMA golden cross, turnover > 5%" — quantitative screening via mootdx.
+ Official Website + Technical Report + Reproduction Repo + Tutorial + Sophub +

+ +

+ Trendshift +

+ +**[English](#-english) · [中文](#-中文)** + +
+ +> 📌 **Official:** GitHub + https://gaagent.ai only. DintalClaw is the sole authorized commercial partner; others are not affiliated. + +--- + + + +## 🌟 Overview + +**GenericAgent** is a minimal, self-evolving autonomous agent framework. Its core is just **~3K lines of code**. Through **9 atomic tools + a ~100-line Agent Loop**, it grants any LLM system-level control over a local computer — covering browser, terminal, filesystem, keyboard/mouse input, screen vision, and mobile devices (ADB). + +> Design philosophy — **don't preload skills, evolve them.** + +Every time GenericAgent solves a new task, it automatically crystallizes the execution path into a reusable **Skill**. The longer you use it, the more skills accumulate — forming a personal skill tree grown entirely from 3K lines of seed code. + +> 🤖 **Self-Bootstrap Proof** — Everything in this repository, from installing Git and running `git init` to every commit message, was completed autonomously by GenericAgent. The author never opened a terminal once. + +### 📑 Table of Contents + +- [Key Features](#-key-features) +- [Demo Showcase](#-demo-showcase) +- [Quick Start](#-quick-start) +- [Usage](#-usage) +- [Unlocking Advanced Capabilities](#-unlocking-advanced-capabilities) +- [Architecture](#-architecture) +- [Self-Evolution Mechanism](#-self-evolution-mechanism) +- [Comparison](#-comparison) +- [Evaluation](#-evaluation) +- [Roadmap & News](#-roadmap--news) +- [Community & Support](#-community--support) +- [License](#-license) + +--- + +## 📋 Key Features + +| Feature | Description | +| :--- | :--- | +| 🧬 **Self-Evolving** | Automatically crystallizes each task into a Skill. Capabilities grow with every use, forming your personal skill tree. | +| 🪶 **Minimal Architecture** | ~3K lines of core code. Agent Loop is ~100 lines. No complex dependencies, zero deployment overhead. | +| ⚡ **Strong Execution** | **TMWebdriver** injects into a real browser (preserving login sessions). 9 atomic tools take direct control of the system. | +| 🔌 **High Compatibility** | Supports Claude / Gemini / Kimi / MiniMax and other major models. Cross-platform. | +| 💰 **Token Efficient** | <30K context window — a fraction of the 200K–1M other agents consume. Less noise, fewer hallucinations, higher success rate, lower cost. | + +--- + +## 🎯 Demo Showcase - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Autonomous web exploration — browses and summarizes on its own schedule.

"Find expenses over ¥2K in the past 3 months" — drives Alipay on a phone via ADB.

WeChat batch messaging — yes, it can drive WeChat too.
🛡️ Real-Browser CAPTCHA Survival🌐 Autonomous Web Exploration
Discord hCaptcha passed in real browserWeb Exploration
While configuring a Discord bot, an hCaptcha "Are you human?" challenge pops up mid-task — GA's real browser session passes it and the task continues. See Browser Realness.Autonomously browses and periodically summarizes web content.
🧋 Food Delivery Order📈 Quantitative Stock Screening
Order TeaStock Selection
"Order me a milk tea" — navigates the delivery app, selects items, completes checkout."Find GEM stocks with EXPMA golden cross, turnover > 5%" — quantitative screening.
💰 Expense Tracking💬 Batch Messaging
Alipay ExpenseWeChat Batch
"Find expenses over ¥2K in the last 3 months" — drives Alipay via ADB.Sends bulk WeChat messages, fully driving the WeChat client.
-## What Happens When You Use It +--- + +## 🚀 Quick Start + +> ⚠️ **Python version**: use **Python 3.11 or 3.12**. **Do not** use Python 3.14 — it is incompatible with `pywebview` and a few other GA dependencies. +> +> 📖 Detailed installation guide: **[installation.md](docs/installation.md)** · **[installation_zh.md(中文)](docs/installation_zh.md)** +### For LLM Agents + +Fetch the installation guide and follow it: + +```bash +curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/refs/heads/main/docs/installation.md ``` -You: "Read my WeChat messages" -Agent: installs dependencies → reverse-engineers DB → writes reader script → saves as SOP -Next time: instant recall, zero setup. -You: "Monitor stock prices and alert me" -Agent: installs mootdx → builds screening workflow → sets up scheduled task → saves as SOP -Next time: one sentence to run. +### For Humans + +#### Method 1 — Clone & install *(recommended)* -You: "Send this file via Gmail" -Agent: configures OAuth → writes send script → saves as SOP -Next time: just works. +```bash +git clone https://github.com/lsdefine/GenericAgent.git && cd GenericAgent +uv venv && uv pip install -e ".[ui]" +cp mykey_template_en.py mykey.py # fill in your LLM API key ``` -**Dogfooding**: This repository — from installing Git to `git init`, writing this README, to every commit message — was built entirely by GenericAgent without the author opening a terminal once. +Dependencies are deliberately tiered: the agent core needs only `requests`, plus four lightweight packages (`beautifulsoup4`, `bottle`, `simple-websocket-server`, `aiohttp`) for TMWebdriver's local server. The `[ui]` extra pulls in frontend libraries (Streamlit, `prompt_toolkit`/`rich` for the TUI, …) — install it for the bundled UIs, or skip it entirely and drive the agent headless. No Playwright, no LangChain, no browser binaries to download. -Every task the agent solves becomes a permanent skill. After a few weeks, your instance has a unique skill tree — grown entirely from 3,300 lines of seed code. +Then launch: -## The Seed Philosophy +```bash +python frontends/tui_v3.py # Terminal UI (recommended) +python launch.pyw # Streamlit web UI +``` -Most agent frameworks ship as finished products. GenericAgent ships as a **seed**. +#### Method 2 — One-line installer *(convenience)* -The 5 core SOPs define how the agent thinks, remembers, and operates. From there, every new capability is discovered and recorded by the agent itself: +Sets up a self-contained directory with an isolated Python environment, Git, and a ready-to-run package. The script is in [`assets/`](assets/) if you'd like to read it first. -1. You ask it to do something new -2. It figures out how (install dependencies, write scripts, test) -3. It saves the procedure as a new SOP in its memory -4. Next time, it recalls and executes directly +**Windows PowerShell** -The agent doesn't just execute — it **learns and remembers**. +```powershell +powershell -ExecutionPolicy Bypass -c "$env:GLOBAL=1; irm https://raw.githubusercontent.com/lsdefine/GenericAgent/main/assets/ga_install.ps1 | iex" +``` -## Quick Start +**Linux / macOS** ```bash -# 1. Clone -git clone https://github.com/lsdefine/pc-agent-loop.git -cd pc-agent-loop +GLOBAL=1 bash -c "$(curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/main/assets/ga_install.sh)" +``` + +> 💡 GenericAgent grows its environment **through the Agent itself** — don't pre-install everything. See [Unlocking Advanced Capabilities](#-unlocking-advanced-capabilities) below. -# 2. Install minimal deps -pip install streamlit pywebview +--- -# 3. Configure API key -cp mykey_template.py mykey.py -# Edit mykey.py with your LLM API key +## 💻 Usage -# 4. Launch -python launch.pyw -``` +### Frontends -**Also runs on Android** — tested successfully on Termux with `python agentmain.py` (CLI frontend): +#### Terminal UI *(recommended)* + +A lightweight, scrollback-first terminal interface built on `prompt_toolkit` + `rich`. Supports multiple concurrent sessions and real-time streaming. ```bash -# In Termux -cd /sdcard/ga -python agentmain.py +python frontends/tui_v3.py ``` -Once running, tell the agent: *"Execute web setup SOP to unlock browser tools"* — it handles the rest. See [WELCOME_NEW_USER.md](WELCOME_NEW_USER.md) for the full bootstrap sequence. +
+⚠️ Windows TUI Troubleshooting + +TUI rendering on Windows can be flaky depending on terminal + font. Common causes: -## vs. Alternatives +1. `prompt_toolkit` / `rich` are not on the latest version — `pip install -U prompt_toolkit rich` first. +2. PowerShell / cmd ship with terminals that have rough Unicode + key-binding support. **Prefer Git Bash on Windows**, which is much better behaved. +3. If it still looks broken, ask GA itself to fix it: + > *"My experience using `frontends/tui_v3.py` in PowerShell / cmd / Git Bash on Windows is very poor — lots of incompatibility. Please refer to Claude Code's best practices for the Windows terminal and fix all font and rendering incompatibilities."* -| | GenericAgent | OpenClaw | Claude Code | -|---|---|---|---| -| Codebase | ~3,300 lines | ~530,000 lines | Open-source (large) | -| Deploy | `pip install` + API key | Multi-service orchestration | CLI + subscription | -| Browser | Injects into real browser (keeps login state) | Sandboxed/headless | Via MCP plugins | -| OS Control | Keyboard, mouse, vision, ADB | Multi-agent delegation | File + terminal | -| Self-evolution | Grows SOPs & tools autonomously | Plugin ecosystem | Stateless per session | -| Core shipped | 10 .py + 5 SOPs | Hundreds of modules | Rich CLI toolkit | +
-## How It Works +#### Streamlit UI +```bash +python launch.pyw ``` -User instruction - ↓ -┌─────────────────────┐ -│ agent_loop.py (92L) │ ← Sense-Think-Act cycle -│ "What do I know? │ -│ What should I do?" │ -└────────┬────────────┘ - ↓ -┌─────────────────────┐ -│ 7 Atomic Tools │ ← All capabilities derive from these -│ code_run │ Execute any Python/PowerShell -│ file_read/write │ Direct disk access -│ file_patch │ Surgical code edits -│ web_scan │ Read live web pages -│ web_execute_js │ Control browser DOM -│ ask_user │ Human-in-the-loop -└────────┬────────────┘ - ↓ -┌─────────────────────┐ -│ Memory System │ ← Persistent across sessions -│ L0: Meta-SOP │ How to manage memory itself -│ L2: Global Facts │ Environment, credentials, paths -│ L3: Task SOPs │ Learned procedures (self-growing) -└─────────────────────┘ + +### Bot Interface (IM) + +GenericAgent also supports IM frontends such as Telegram, Discord, and Lark. + +| Platform | Command | +| :--- | :--- | +| Telegram | `python frontends/tgapp.py` | +| Discord | `python frontends/dcapp.py` | +| Lark / Feishu | `python frontends/fsapp.py` | + +> WeChat, QQ, WeCom and DingTalk are also supported — see the Chinese section below. +> For detailed setup, ask GenericAgent itself. + +--- + +## 🔓 Unlocking Advanced Capabilities + +In GA, advanced capabilities are unlocked by **instructing the agent**, not by reading +docs or installing extras. Each instruction below makes GA read its pre-installed SOPs +(battle-tested playbooks in its memory), install whatever is missing, adapt to your OS, +and persist the result into its own memory. + +| Capability | Just tell GA | +| :--- | :--- | +| 🌐 Web automation | *"Set up your web automation capability."* — GA guides you through the one manual step: dragging the bundled Chrome extension into `chrome://extensions`. | +| 🔤 OCR | *"Set up your OCR capability with rapidocr and save it to memory."* | +| 👁️ Vision | *"Set up your vision capability from the template in memory/."* — GA copies the template, wires it to your existing LLM keys, and self-tests. | +| 🖱️ Computer use | *"Probe this system and set up your computer-use capability."* | + +> 💡 **About language**: the pre-installed SOPs are written in Chinese — GA reads them +> natively, so this never blocks you. If you prefer an English knowledge base, just say: +> *"Read your pre-installed SOPs and rewrite them in English (keep code, paths and error +> strings verbatim)."* +> +> 🌍 **About platforms**: the SOPs were honed on Windows, but cross-platform adaptation is +> itself a GA task — on macOS/Linux, GA swaps in the platform equivalents (window +> enumeration, input control, screenshots) on its own. Same self-evolution principle. + +--- + +## 🧠 Architecture + +GenericAgent accomplishes complex tasks through **Layered Memory × Minimal Toolset × Autonomous Execution Loop**, continuously accumulating experience during execution. + +### 1️⃣ Layered Memory System + +> *Memory crystallizes throughout task execution, letting the agent build stable, efficient working patterns over time.* + +| Layer | Name | Description | +| :---: | :--- | :--- | +| **L0** | Meta Rules | Core behavioral rules and system constraints | +| **L1** | Insight Index | Minimal memory index for fast routing and recall | +| **L2** | Global Facts | Stable knowledge accumulated over long-term operation | +| **L3** | Task Skills / SOPs | Reusable workflows for completing specific task types | +| **L4** | Session Archive | Archived task records distilled from finished sessions for long-horizon recall | + +### 2️⃣ Autonomous Execution Loop + +> *Perceive environment state → Task reasoning → Execute tools → Write experience to memory → Loop* + +The entire core loop is just **~100 lines of code** ([`agent_loop.py`](agent_loop.py)). + +### 3️⃣ Minimal Toolset + +> *GenericAgent provides only **9 atomic tools**, forming the foundational capabilities for interacting with the outside world.* + +| Tool | Function | +| :--- | :--- | +| `code_run` | Execute arbitrary code (Python / PowerShell) | +| `file_read` | Read files | +| `file_write` | Write / create / overwrite files | +| `file_patch` | Patch / modify files | +| `web_scan` | Perceive web content | +| `web_execute_js` | Control browser behavior | +| `ask_user` | Human-in-the-loop confirmation | +| `update_working_checkpoint` | *(memory)* Short-term working notepad | +| `start_long_term_update` | *(memory)* Distill long-term memory | + +### 4️⃣ Capability Extension + +> *Capable of dynamically creating new tools.* + +Via `code_run`, GenericAgent can dynamically install Python packages, write new scripts, call external APIs, or control hardware at runtime — crystallizing temporary abilities into permanent tools. + +
+ GenericAgent Workflow +
GenericAgent Workflow Diagram +
+ +--- + +## 🧬 Self-Evolution Mechanism + +This is what fundamentally distinguishes GenericAgent from every other agent framework. + +```text +[New Task] + │ + ▼ +[Autonomous Exploration] ─► install deps · write scripts · debug · verify + │ + ▼ +[Crystallize into Skill] ─► write to memory layer + │ + ▼ +[Direct Recall on Next Similar Task] ``` -The agent starts with 7 primitive tools. Through `code_run`, it can install packages, write scripts, and interface with any hardware or API — effectively manufacturing new tools at runtime. +| What you say | First time | Every time after | +| :--- | :--- | :--- | +| *"Read my WeChat messages"* | Install deps → reverse DB → write read script → save Skill | **one-line invoke** | +| *"Give me a morning digest of Hacker News"* | Write scraper → build digest → schedule daily run → save Skill | **one-line invoke** | +| *"Monitor stocks and alert me"* | Install `mootdx` → build selection flow → configure cron → save Skill | **one-line start** | +| *"Send this file via Gmail"* | Configure OAuth → write send script → save Skill | **ready to use** | -
-What Ships in the Box +After a few weeks, your agent instance will have a skill tree no one else in the world has — all grown from 3K lines of seed code. -**Core engine** (runs the agent): -- `agent_loop.py` — Sense-Think-Act loop (92 lines) -- `ga.py` — Tool definitions and execution -- `sidercall.py` — LLM communication (multi-backend) -- `agentmain.py` — Session orchestration +--- -**Interface** (talk to the agent): -- `stapp.py` — Streamlit web UI -- `tgapp.py` — Telegram bot interface -- `launch.pyw` — One-click launcher with floating window +## 📊 Comparison -**Infrastructure**: -- `TMWebDriver.py` — Browser injection bridge (not Selenium — injects JS into your real browser via Tampermonkey) -- `simphtml.py` — HTML→text cleaner for web perception +| Feature | **GenericAgent** | OpenClaw | Claude Code | +| :--- | :---: | :---: | :---: | +| **Codebase** | ~3K lines | ~530,000 lines | Open-sourced (large) | +| **Deployment** | `pip install` + API Key | Multi-service orchestration | CLI + subscription | +| **Browser Control** | Real browser (session preserved) | Sandbox / headless browser | Via MCP plugin | +| **OS Control** | Mouse/kbd, vision, ADB | Multi-agent delegation | File + terminal | +| **Self-Evolution** | Autonomous skill growth | Plugin ecosystem | Stateless between sessions | +| **Out of the Box** | Few core files + starter skills | Hundreds of modules | Rich CLI toolset | -**5 Core SOPs** (shipped, version-controlled): -1. `memory_management_sop` — L0 constitution: how the agent manages its own memory -2. `autonomous_operation_sop` — Self-directed task execution -3. `scheduled_task_sop` — Cron-like recurring tasks -4. `web_setup_sop` — Browser environment bootstrap -5. `ljqCtrl_sop` — Desktop physical control (keyboard, mouse, DPI-aware) +--- -Everything else — Gmail integration, WeChat automation, vision APIs, game downloaders, stock analysis workflows — the agent builds and memorizes on its own through use. +## 📈 Evaluation -
+> 📂 Full evaluation datasets and results: [**JinyiHan99/GA-Technical-Report**](https://github.com/JinyiHan99/GA-Technical-Report/tree/main) + +We evaluate GenericAgent across **five dimensions**: + +| # | Dimension | Question | Benchmarks | +| :---: | :--- | :--- | :--- | +| 1 | **Task Completion & Token Efficiency** | Can GA complete hard tasks more cheaply than leading agents? | SOP-Bench, Lifelong AgentBench, RealFin-Benchmark | +| 2 | **Tool-Use Efficiency** | Can a minimal atomic toolset solve what specialized toolsets solve, with less overhead? | Tool Efficiency Benchmark (11 simple + 5 long-horizon) | +| 3 | **Memory System Effectiveness** | Does condensed hierarchical memory beat full/redundant memory and embedding-based retrievers? | SOP-Bench (dangerous goods), LoCoMo, 20-skill stress test | +| 4 | **Self-Evolution Capability** | Can the agent distill experience into reusable SOPs and code, without intervention? | 9-round LangChain longitudinal study, 8-task cross-task web benchmark | +| 5 | **Web Browsing Capability** | Does density-driven design survive the open web? | WebCanvas, BrowseComp-ZH, Custom Tasks (22) | + +Baselines across these dimensions include **Claude Code**, **OpenAI CodeX**, and **OpenClaw**, evaluated under *Claude Sonnet 4.6*, *Claude Opus 4.6*, *GPT-5.4*, and *MiniMax M2.7* backbones. + + + + + + +
+ Tool-use efficiency radar
+ Tool-use efficiency radar. GA dominates token, request, and tool-call axes while preserving quality across four task dimensions. +
+ Cross-task self-evolution convergence
+ Cross-task self-evolution. Second- and third-run GA executions converge to a stable low-cost regime across eight web tasks, while OpenClaw shows no such convergence. +
+ +### Browser Realness of GA Web Tools (TMWebdriver) + +GA web tools are powered by **TMWebdriver** — a local WebSocket server plus a Chrome extension — running through a **real, persistent Chrome/Chromium session** rather than a disposable headless sandbox, preserving cookies, login state, extensions, GPU/WebGL behavior, and normal browser-session fingerprints. + +| Detection Service / Signal | Vanilla Headless Automation | GA Web Tools | Notes | +| :--- | :---: | :---: | :--- | +| SannySoft headless test | Often detected | ✅ 56/56 passed | `bot.sannysoft.com` | +| bot.incolumitas.com | Commonly fails webdriver / CDP checks | ✅ 36/36 passed | `WEBDRIVER`, `SELENIUM_DRIVER`, `webDriverAdvanced` all OK | +| BrowserScan bot detection | Often abnormal | ✅ Normal | `browserscan.net` | +| Device & Browser Info bot test | Multiple bot flags | ✅ Human / `isBot=false` | `deviceandbrowserinfo.com` | +| FingerprintJS bot detection demo | Often detected | ✅ Passed | Demo flow completed without bot verdict | +| reCAPTCHA v3 demo | Low bot-like score | ✅ 0.9 human-like score | Score-based risk signal; 0.9 is above typical production thresholds | + +For reCAPTCHA v3, `0.9` is not a "checkbox solved" result; it is the high-confidence human-like score returned by the risk model, typically sufficient to avoid extra challenges in production flows. + +--- + +## 📅 Roadmap & News + +- **2026-05-23** — 🆕 **TUI v3 released** (`frontends/tui_v3.py`). Block-based scrollback with proper resize reflow, per-terminal color profile for cross-terminal parity, and feature parity with v2. +- **2026-05-18** — 🆕 **Morphling mode**. Project-level skill absorption — extract goal + tests from any external repo, then decide per component: call, rewrite, or discard. See `memory/morphling_sop.md`. +- **2026-05-17** — 🆕 **Goal Hive mode**. Multi-worker cooperative Goal mode — BBS-coordinated master/workers running long-horizon objectives in parallel. See `memory/goal_hive_sop.md`. +- **2026-05-15** — 🖥️ **Desktop GUI released**. One-line installs ship a ready-to-run desktop app (`frontends/GenericAgent.exe`). Developers launch via `python launch.pyw`. +- **2026-05-14** — 🆕 **Conductor sub-agent orchestration**. Spawn, supervise, and auto-clean parallel sub-agents; first-class delegation primitives complementing `/btw` side-questions. +- **2026-05-12** — 🆕 **TUI v2 released** (`frontends/tuiapp_v2.py`). Refined Textual frontend with image-paste folding, file paste, block-delete, Ctrl+C copy, history navigation, and `/llm` / `/export` / `/continue` pickers. +- **2026-05-08** — 🆕 **Goal mode** (`reflect/goal_mode.py`). Time-budget-driven self-driven loop — "keep optimizing X for N hours" with no premature delivery. +- **2026-04-21** — 📄 [**Technical Report on arXiv**](https://arxiv.org/abs/2604.17091) — *GenericAgent: A Token-Efficient Self-Evolving LLM Agent via Contextual Information Density Maximization*. +- **2026-04-11** — Introduced **L4 session archive memory** and scheduler cron integration. +- **2026-03-23** — Personal WeChat supported as a bot frontend. +- **2026-03-10** — [Released million-scale Skill Library](https://mp.weixin.qq.com/s/q2gQ7YvWoiAcwxzaiwpuiQ?scene=1&click_id=7) *(Chinese)*. +- **2026-03-08** — [Released "Dintal Claw" — a GenericAgent-powered government-affairs bot](https://mp.weixin.qq.com/s/eiEhwo-j6S-WpLxgBnNxBg) *(Chinese)*. +- **2026-03-01** — [Featured by Jiqizhixin (机器之心)](https://mp.weixin.qq.com/s/uVWpTTF5I1yzAENV_qm7yg) *(Chinese)*. +- **2026-01-16** — GenericAgent **V1.0** public release. --- - +## ⭐ Community & Support -# GenericAgent — 3,300 行代码,完整 OS 级自主控制 +If this project helped you, please consider leaving a **Star!** 🙏 + +### 🚩 Friendly Links + +Thanks to the **LinuxDo** community for the support! + +[![LinuxDo](https://img.shields.io/badge/Community-LinuxDo-blue?style=for-the-badge)](https://linux.do/) + +**Community GUIs** *(independent open-source projects)*: + +- [chilishark27/ga-manager](https://github.com/chilishark27/ga-manager) +- [wangjc683/galley](https://github.com/wangjc683/galley) — Out-of-the-box local agent workbench with a bundled GA runtime (CPython 3.11 + deps), native GUI/CLI, multi-session + Project orchestration, local-first. +- [FroStorM/A3Agent](https://github.com/FroStorM/A3Agent/tree/workbench) +- [Fwind43/GenericAgent-Admin](https://github.com/Fwind43/GenericAgent-Admin) — Go + React desktop admin panel: service lifecycle management, native chat, Goal mode, BBS team board, file editor, model config wizard, TMWebDriver monitor, self-update, and Windows tray/desktop-pet integration. + +--- + +## 📄 License + +Distributed under the **MIT License**. See [`LICENSE`](LICENSE) for full text. + +> *Disclaimer: The official GenericAgent channels are this GitHub repository and https://gaagent.ai. DintalClaw is currently the only officially authorized commercial partner; any other third-party website, organization, or individual using the GenericAgent name is not official unless explicitly listed here.* + +--- -一个极简自主 Agent 框架。用约 3,300 行 Python,让任意 LLM 获得对你 PC 的物理级控制能力——浏览器、终端、文件系统、键鼠、屏幕视觉、移动设备。 + -不需要 Electron,不需要 Docker,不需要 Mac Mini,不需要 53 万行代码,不需要付费安装服务。 +## 🌟 项目简介 -## 用起来是什么样的 +**GenericAgent** 是一个极简、可自我进化的自主 Agent 框架。核心仅 **~3K 行代码**,通过 **9 个原子工具 + ~100 行 Agent Loop**,赋予任意 LLM 对本地计算机的系统级控制能力,覆盖浏览器、终端、文件系统、键鼠输入、屏幕视觉及移动设备(ADB)。 +> 设计哲学 —— **不预设技能,靠进化获得能力。** + +每解决一个新任务,GenericAgent 就将执行路径自动固化为 Skill,供后续直接调用。使用时间越长,沉淀的技能越多,形成一棵完全属于你、从 3K 行种子代码生长出来的专属技能树。 + +> 🤖 **自举实证** — 本仓库的一切,从安装 Git、`git init` 到每一条 commit message,均由 GenericAgent 自主完成。作者全程未打开过一次终端。 + +### 📑 目录 + +- [核心特性](#-核心特性) +- [实例展示](#-实例展示) +- [快速开始](#-快速开始) +- [使用方式](#-使用方式) +- [架构设计](#-架构设计) +- [自我进化机制](#-自我进化机制) +- [与同类产品对比](#-与同类产品对比) +- [评测](#-评测) +- [路线图与最新动态](#-路线图与最新动态) +- [社区与支持](#-社区与支持) +- [许可](#-许可) + +--- + +## 📋 核心特性 + +| 特性 | 说明 | +| :--- | :--- | +| 🧬 **自我进化** | 每次任务自动沉淀 Skill,能力随使用持续增长,形成专属技能树 | +| 🪶 **极简架构** | ~3K 行核心代码,Agent Loop 约百行,无复杂依赖,部署零负担 | +| ⚡ **强执行力** | 注入真实浏览器(保留登录态),9 个原子工具直接接管系统 | +| 🔌 **高兼容性** | 支持 Claude / Gemini / Kimi / MiniMax 等主流模型,跨平台运行 | +| 💰 **极致省 Token** | 上下文窗口不到 30K,是其他 Agent(200K–1M)的零头;噪声更少、幻觉更低、成功率更高,成本低一个数量级 | + +--- + +## 🎯 实例展示 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
🧋 外卖下单📈 量化选股
外卖下单量化选股
"Order me a milk tea" — 自动导航外卖 App,选品并完成结账"Find GEM stocks with EXPMA golden cross, turnover > 5%" — 量化条件筛股
🌐 自主网页探索💰 支出追踪
网页探索支付宝支出
自主浏览并定时汇总网页信息"查找近 3 个月超 ¥2K 的支出" — 通过 ADB 驱动支付宝
💬 批量消息
微信批量
批量发送微信消息,完整驱动微信客户端
+ +--- + +## 🚀 快速开始 + +> ⚠️ **Python 版本:** 推荐使用 **Python 3.11 或 3.12**。**请不要使用 Python 3.14**,与 `pywebview` 及部分依赖不兼容。 +> +> 📖 详细安装指南:**[installation_zh.md(中文)](docs/installation_zh.md)** · **[installation.md (English)](docs/installation.md)** + +### 给 LLM Agent 看的 + +获取安装指南并照做: + +```bash +curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/refs/heads/main/docs/installation_zh.md +``` + +### 给人类用户看的 + +#### 方法一 — 一键安装 *(推荐)* + +一键安装会自动准备独立 Python 环境、Git、项目文件和桌面端,不污染系统环境。 + +**Windows PowerShell** + +```powershell +powershell -ExecutionPolicy Bypass -c "irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" ``` -你:"帮我读取微信消息" -Agent:安装依赖 → 逆向数据库 → 写读取脚本 → 保存为 SOP -下次:一句话直接调用,零配置。 -你:"帮我监控股票并提醒" -Agent:安装 mootdx → 构建选股工作流 → 设置定时任务 → 保存为 SOP -下次:一句话启动。 +**Linux / macOS** -你:"用 Gmail 发这个文件" -Agent:配置 OAuth → 写发送脚本 → 保存为 SOP -下次:直接能用。 +```bash +curl -fsSL http://fudankw.cn:9000/files/ga_install.sh | bash ``` -**自举实证**:本仓库从安装 Git、`git init`、编写 README 到每一条 commit message,全程由 GenericAgent 完成——作者没有打开过一次终端。 +安装完成后启动: -每个解决过的任务都会变成永久技能。用几周后,你的 Agent 实例会拥有一套独特的技能树——全部从 3,300 行种子代码中生长出来。 +- **Windows** — 双击 `frontends/GenericAgent.exe` +- **Linux / macOS** — 在安装目录运行 `python launch.pyw` -## 自举哲学 +#### 方法二 — Python 安装 *(开发者)* -多数 Agent 框架以成品形态发布。GenericAgent 以**种子**形态发布。 +```bash +git clone https://github.com/lsdefine/GenericAgent.git +cd GenericAgent +uv venv +uv pip install -e ".[ui]" # 核心 + UI 依赖 +cp mykey_template.py mykey.py # 填入你的 LLM API Key +python launch.pyw +``` -5 个核心 SOP 定义了 Agent 如何思考、记忆和行动。之后的一切能力,由 Agent 在使用中自主发现并记录: +> 💡 GenericAgent 更推荐由 **Agent 在使用中自举环境**,而不是预先手动装完整依赖。 -1. 你让它做一件新事 -2. 它自己摸索方法(安装依赖、写脚本、测试) -3. 把流程保存为新 SOP -4. 下次直接调用 +📖 完整引导流程见 [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md) +📖 新手图文版:[飞书文档](https://my.feishu.cn/wiki/CGrDw0T76iNFuskmwxdcWrpinPb) +📘 完整入门教程(Datawhale 出品):[Hello GenericAgent](https://datawhalechina.github.io/hello-generic-agent/) · [GitHub](https://github.com/datawhalechina/hello-generic-agent) -Agent 不只是执行——它**学习并记忆**。 +--- + +## 💻 使用方式 + +### 前端启动 -## 快速开始 +#### 桌面端 + +一键安装自带桌面端(Windows),双击: + +```text +frontends/GenericAgent.exe +``` + +#### 终端 UI + +基于 [Textual](https://github.com/Textualize/textual) 的轻量键盘驱动界面。支持多会话并发、实时流式输出,有终端就能跑。 ```bash -# 1. 克隆 -git clone https://github.com/lsdefine/pc-agent-loop.git -cd pc-agent-loop +python frontends/tuiapp_v2.py +``` + +
+⚠️ Windows 上 TUI 显示异常的排查思路 + +1. `textual` 版本太旧,先 `pip install -U textual`; +2. PowerShell / cmd 自带终端对 Unicode 和键位的支持比较糟糕,**Windows 上推荐用 Git Bash**,体验明显更稳; +3. 仍然显示异常时,可以让 GA 自己修一遍,参考 Prompt: + > *"我在 Windows 的 PowerShell / cmd / Git Bash 中使用 `frontends/tuiapp_v2.py` 体验非常差,出现了一堆不兼容问题。请参考 Claude Code 在 Windows 终端的最佳配置,把所有字体和显示不兼容的问题修一遍。"* -# 2. 安装最小依赖 -pip install streamlit pywebview +
-# 3. 配置 API Key -cp mykey_template.py mykey.py -# 编辑 mykey.py 填入你的 LLM API Key +#### Streamlit UI -# 4. 启动 +```bash python launch.pyw ``` -**同样可在 Android 上运行** — 已在 Termux 上测试通过,通过 `python agentmain.py`(CLI 前端)启动: +### Bot 接口(IM) -```bash -# 在 Termux 中 -cd /sdcard/ga -python agentmain.py +GenericAgent 支持 Telegram、Discord、微信、QQ、飞书 / Lark、企业微信、钉钉等 IM 前端。 + +| 平台 | 启动命令 | +| :--- | :--- | +| Telegram | `python frontends/tgapp.py` | +| Discord | `python frontends/dcapp.py` | +| 微信 | `python frontends/wechatapp.py` | +| QQ | `python frontends/qqapp.py` | +| 飞书 / Lark | `python frontends/fsapp.py` | +| 企业微信 | `python frontends/wecomapp.py` | +| 钉钉 | `python frontends/dingtalkapp.py` | + +> 详细配置直接问 GenericAgent。 + +--- + +## 🧠 架构设计 + +GenericAgent 通过 **分层记忆 × 最小工具集 × 自主执行循环** 完成复杂任务,并在执行过程中持续积累经验。 + +### 1️⃣ 分层记忆系统 + +> *记忆在任务执行过程中持续沉淀,使 Agent 逐步形成稳定且高效的工作方式。* + +| 层级 | 名称 | 说明 | +| :---: | :--- | :--- | +| **L0** | 元规则(Meta Rules) | Agent 的基础行为规则和系统约束 | +| **L1** | 记忆索引(Insight Index) | 极简索引层,用于快速路由与召回 | +| **L2** | 全局事实(Global Facts) | 在长期运行过程中积累的稳定知识 | +| **L3** | 任务 Skills / SOPs | 完成特定任务类型的可复用流程 | +| **L4** | 会话归档(Session Archive) | 从已完成任务中提炼出的归档记录,用于长程召回 | + +### 2️⃣ 自主执行循环 + +> *感知环境状态 → 任务推理 → 调用工具执行 → 经验写入记忆 → 循环* + +整个核心循环仅 **约百行代码**([`agent_loop.py`](agent_loop.py))。 + +### 3️⃣ 最小工具集 + +> *GenericAgent 仅提供 **9 个原子工具**,构成与外部世界交互的基础能力。* + +| 工具 | 功能 | +| :--- | :--- | +| `code_run` | 执行任意代码(Python / PowerShell) | +| `file_read` | 读取文件 | +| `file_write` | 写入 / 创建 / 覆盖文件 | +| `file_patch` | 修改文件 | +| `web_scan` | 感知网页内容 | +| `web_execute_js` | 控制浏览器行为 | +| `ask_user` | 人机协作确认 | +| `update_working_checkpoint` | *(记忆)* 短期工作记事板 | +| `start_long_term_update` | *(记忆)* 提炼长期记忆 | + +### 4️⃣ 能力扩展机制 + +> *具备动态创建新工具的能力。* + +通过 `code_run`,GenericAgent 可在运行时动态安装 Python 包、编写新脚本、调用外部 API 或控制硬件,将临时能力固化为永久工具。 + +
+ GenericAgent 工作流程 +
GenericAgent 工作流程图 +
+ +--- + +## 🧬 自我进化机制 + +这是 GenericAgent 区别于其他 Agent 框架的根本所在。 + +```text +[遇到新任务] + │ + ▼ +[自主摸索] ─► 安装依赖 · 编写脚本 · 调试验证 + │ + ▼ +[执行路径固化为 Skill] ─► 写入记忆层 + │ + ▼ +[下次同类任务直接调用] ``` -启动后告诉 Agent:"执行 web setup SOP 解锁浏览器工具"——剩下的它自己搞定。完整引导流程见 [WELCOME_NEW_USER.md](WELCOME_NEW_USER.md)。 +| 你说的一句话 | 第一次做了什么 | 之后每次 | +| :--- | :--- | :--- | +| *"监控股票并提醒我"* | 安装 `mootdx` → 构建选股流程 → 配置定时任务 → 保存 Skill | **一句话启动** | +| *"用 Gmail 发这个文件"* | 配置 OAuth → 编写发送脚本 → 保存 Skill | **直接可用** | -## 对比 +用几周后,你的 Agent 实例将拥有一套任何人都没有的专属技能树,全部从 3K 行种子代码中生长而来。 -| | GenericAgent | OpenClaw | Claude Code | -|---|---|---|---| -| 代码量 | ~3,300 行 | ~530,000 行 | 已开源(体量大) | -| 部署 | `pip install` + API key | 多服务编排 | CLI + 订阅 | -| 浏览器 | 注入真实浏览器(保留登录态) | 沙箱/无头浏览器 | 通过 MCP 插件 | -| OS 控制 | 键鼠、视觉、ADB | 多 Agent 委派 | 文件 + 终端 | -| 自我进化 | 自主生长 SOP 和工具 | 插件生态 | 会话间无状态 | -| 出厂配置 | 10 个 .py + 5 个 SOP | 数百模块 | 丰富 CLI 工具集 | +--- -## 工作原理 +## 📊 与同类产品对比 -Agent 拥有 7 个原子工具:`code_run`(执行任意代码)、`file_read/write/patch`(文件操作)、`web_scan`(网页感知)、`web_execute_js`(浏览器控制)、`ask_user`(人机协作)。 +| 特性 | **GenericAgent** | OpenClaw | Claude Code | +| :--- | :---: | :---: | :---: | +| **代码量** | ~3K 行 | ~530,000 行 | 已开源(体量大) | +| **部署方式** | `pip install` + API Key | 多服务编排 | CLI + 订阅 | +| **浏览器控制** | 注入真实浏览器(保留登录态) | 沙箱 / 无头浏览器 | 通过 MCP 插件 | +| **OS 控制** | 键鼠、视觉、ADB | 多 Agent 委派 | 文件 + 终端 | +| **自我进化** | 自主生长 Skill 和工具 | 插件生态 | 会话间无状态 | +| **出厂配置** | 几个核心文件 + 少量初始 Skills | 数百模块 | 丰富 CLI 工具集 | -通过 `code_run`,它可以安装任何包、编写任何脚本、对接任何硬件——相当于在运行时制造新工具。学到的流程保存为 SOP,下次直接调用。 +--- -核心循环只有 92 行(`agent_loop.py`):感知 → 思考 → 行动 → 记忆。 +## 📈 评测 -
-出厂清单 +> 📂 完整的评测数据集以及评测结果见:[**JinyiHan99/GA-Technical-Report**](https://github.com/JinyiHan99/GA-Technical-Report/tree/main) -**核心引擎**: -- `agent_loop.py` — 感知-思考-行动循环(92 行) -- `ga.py` — 工具定义与执行 -- `sidercall.py` — LLM 通信(多后端) -- `agentmain.py` — 会话编排 +我们从 **五大维度** 评测 GenericAgent: -**交互界面**: -- `stapp.py` — Streamlit Web UI -- `tgapp.py` — Telegram 机器人 -- `launch.pyw` — 一键启动 + 悬浮窗 +| # | 维度 | 核心问题 | 使用的基准 | +| :---: | :--- | :--- | :--- | +| 1 | **任务完成度与 Token 效率** | GA 能否以更低成本完成高难度任务? | SOP-Bench、Lifelong AgentBench、RealFin-Benchmark | +| 2 | **工具使用效率** | 最小原子工具集能否以更低开销替代专用工具集? | Tool Efficiency Benchmark | +| 3 | **记忆系统有效性** | 精简分层记忆能否超越冗余记忆和基于 Embedding 的检索器? | SOP-Bench、LoCoMo、20-skill 压力测试 | +| 4 | **自我进化能力** | Agent 能否在无人干预下将经验提炼为可复用的 SOP 与代码? | 9 轮 LangChain 纵向研究、8 任务跨任务 Web 基准 | +| 5 | **网页浏览能力** | 信息密度驱动设计能否适应开放网页? | WebCanvas、BrowseComp-ZH、自定义任务 | -**基础设施**: -- `TMWebDriver.py` — 浏览器注入桥接(非 Selenium,通过 Tampermonkey 注入真实浏览器) -- `simphtml.py` — HTML→文本清洗 +以上维度的基线包括 **Claude Code**、**OpenAI CodeX** 和 **OpenClaw**,分别在 *Claude Sonnet 4.6*、*Claude Opus 4.6*、*GPT-5.4* 和 *MiniMax M2.7* 底座上进行评测。 -**5 个核心 SOP**(出厂自带,版本控制): -1. `memory_management_sop` — L0 宪法:Agent 如何管理自身记忆 -2. `autonomous_operation_sop` — 自主任务执行 -3. `scheduled_task_sop` — 定时任务 -4. `web_setup_sop` — 浏览器环境引导 -5. `ljqCtrl_sop` — 桌面物理控制(键鼠、DPI 感知) + + + + + +
+ 工具使用效率雷达图
+ 工具使用效率雷达图。GA 在 Token、请求数和工具调用轴上全面领先,同时在四个任务维度上保持质量。 +
+ 跨任务自我进化收敛曲线
+ 跨任务自我进化。GA 的第二轮和第三轮执行在 8 个 Web 任务上收敛至稳定的低成本区间。 +
-其余一切——Gmail、微信自动化、视觉 API、游戏下载、股票分析——都是 Agent 在使用中自主构建并记忆的。 +### GA Web 工具的浏览器真实性 -
+GA Web 工具运行在**真实、持久化的 Chrome/Chromium 会话**中,而不是一次性的 headless 沙箱,因此可以保留 Cookie、登录态、扩展、GPU/WebGL 行为以及正常浏览器会话指纹。 + +| 检测服务 / 信号 | 普通 Headless 自动化 | GA Web 工具 | 说明 | +| :--- | :---: | :---: | :--- | +| SannySoft headless test | 常被识别 | ✅ 56/56 通过 | `bot.sannysoft.com` | +| bot.incolumitas.com | 常在 webdriver / CDP 项异常 | ✅ 36/36 通过 | `WEBDRIVER`、`SELENIUM_DRIVER`、`webDriverAdvanced` 全部 OK | +| BrowserScan bot detection | 常显示异常 | ✅ Normal | `browserscan.net` | +| Device & Browser Info bot test | 多个 bot 标记 | ✅ Human / `isBot=false` | `deviceandbrowserinfo.com` | +| FingerprintJS bot detection demo | 常被识别 | ✅ 通过 | Demo 流程完成,未给出 bot 判定 | +| reCAPTCHA v3 demo | 低分 / bot-like | ✅ 0.9 真人相似分 | v3 是基于分数的风险信号;0.9 高于常见生产阈值 | + +对于 reCAPTCHA v3,`0.9` 不是“点过验证码”的结果,而是风控模型返回的高置信真人相似分,通常足以通过生产环境中的常见阈值,避免进入更严格挑战。 + +--- + +## 📅 路线图与最新动态 + +- **2026-05-23** — 🆕 **TUI v3 正式发布**(`frontends/tui_v3.py`)。基于块的滚屏回看 + 正确的 resize 重排,每终端独立配色保证跨终端一致,并与 v2 达成功能对齐。 +- **2026-05-18** — 🆕 **Morphling 模式**。项目级能力吞噬 —— 从任意外部仓库抽取目标与测例后,对每个核心组件分别决定调用、重写或舍弃。详见 `memory/morphling_sop.md`。 +- **2026-05-17** — 🆕 **Goal Hive 模式**。多 worker 协作版 Goal —— Master/Worker 通过 BBS 协同推进长程目标。详见 `memory/goal_hive_sop.md`。 +- **2026-05-15** — 🖥️ **桌面 GUI 发布**。一键安装会自带可直接运行的桌面端(`frontends/GenericAgent.exe`),开发者也可用 `python launch.pyw` 启动。 +- **2026-05-14** — 🆕 **Conductor 子 Agent 编排**。派发、监督、自动清理并行子 Agent;与 `/btw` 旁路子 Agent 互补,提供一等公民级的任务委派原语。 +- **2026-05-12** — 🆕 **TUI v2 正式发布**(`frontends/tuiapp_v2.py`)。重做视觉风格的 Textual 前端,支持图片粘贴折叠、文件粘贴、块删除、Ctrl+C 复制、历史导航,以及 `/llm` / `/export` / `/continue` 选择器。 +- **2026-05-08** — 🆕 **Goal 模式**(`reflect/goal_mode.py`)。时间预算驱动的自驱循环 —— "持续优化 X N 小时",预算没到不准提前交付。 +- **2026-04-21** — 📄 [**技术报告已发布至 arXiv**](https://arxiv.org/abs/2604.17091) — *GenericAgent: A Token-Efficient Self-Evolving LLM Agent via Contextual Information Density Maximization*。 +- **2026-04-11** — 引入 **L4 会话归档记忆**,并接入 scheduler cron 调度。 +- **2026-03-23** — 支持个人微信接入作为 Bot 前端。 +- **2026-03-10** — [发布百万级 Skill 库](https://mp.weixin.qq.com/s/q2gQ7YvWoiAcwxzaiwpuiQ?scene=1&click_id=7)。 +- **2026-03-08** — [发布以 GenericAgent 为核心的"政务龙虾" Dintal Claw](https://mp.weixin.qq.com/s/eiEhwo-j6S-WpLxgBnNxBg)。 +- **2026-03-01** — [被机器之心报道](https://mp.weixin.qq.com/s/uVWpTTF5I1yzAENV_qm7yg)。 +- **2026-01-16** — GenericAgent **V1.0** 公开版本发布。 + +--- + +## ⭐ 社区与支持 + +如果这个项目对你有帮助,欢迎点一个 **Star!** 🙏 + +也欢迎加入 **GenericAgent 体验交流群**,一起交流、反馈、共建 👏 + +
+ + + + +
微信群 21
微信群 21 二维码
+
+ +### 🚩 友情链接 + +感谢 **LinuxDo** 社区的支持! + +[![LinuxDo](https://img.shields.io/badge/社区-LinuxDo-blue?style=for-the-badge)](https://linux.do/) + +**社区 GUI 客户端** *(独立开源项目)*: + +- [chilishark27/ga-manager](https://github.com/chilishark27/ga-manager) +- [wangjc683/galley](https://github.com/wangjc683/galley) —— 开箱即用的本地 Agent 工作台,自带 GA 内核(内置 CPython 3.11 + 运行依赖),GUI/CLI 双原生、多 session + Project 编排、本地优先。 +- [FroStorM/A3Agent](https://github.com/FroStorM/A3Agent/tree/workbench) +- [Fwind43/GenericAgent-Admin](https://github.com/Fwind43/GenericAgent-Admin) —— Go + React 桌面管理面板:服务生命周期管理、原生 Chat、Goal 模式、BBS 团队看板、文件编辑器、模型配置向导、TMWebDriver 监控、自更新,以及 Windows 托盘/桌面宠物集成。 + +--- + +## 📄 许可 + +基于 **MIT License** 发布,详见 [`LICENSE`](LICENSE)。 + +> *声明:GenericAgent 官方渠道为本 GitHub 仓库和 https://gaagent.ai。DintalClaw 是目前唯一官方授权的商业合作方;除非在此处明确列出,其他使用 GenericAgent 名义的第三方网站、机构、组织或个人均非官方。* + +--- + +## 📈 Star History + +
-## 许可 + + + + + Star History Chart + + -MIT \ No newline at end of file +

+
diff --git a/TMWebDriver.py b/TMWebDriver.py index 86a2b0b05..c14535a18 100644 --- a/TMWebDriver.py +++ b/TMWebDriver.py @@ -1,9 +1,8 @@ import json, threading, time, uuid, queue, socket, requests, traceback -from typing import Dict, Any, Optional, List -from simple_websocket_server import WebSocketServer, WebSocket -from bs4 import BeautifulSoup -import bottle, random -from bottle import route, template, request, response +from typing import Any +from simple_websocket_server import WebSocketServer, WebSocket +import bottle +from bottle import request class Session: def __init__(self, session_id, info, client=None): @@ -12,7 +11,7 @@ def __init__(self, session_id, info, client=None): self.connect_at = time.time() self.disconnect_at = None self.type = info.get('type', 'ws') - self.ws_client = client if self.type == 'ws' else None + self.ws_client = client if self.type in ('ws', 'ext_ws') else None self.http_queue = client if self.type == 'http' else None @property def url(self): return self.info.get('url', '') @@ -22,7 +21,7 @@ def is_active(self): def reconnect(self, client, info): self.info = info self.type = info.get('type', 'ws') - if self.type == 'ws': + if self.type in ('ws', 'ext_ws'): self.ws_client = client self.http_queue = None elif self.type == 'http': @@ -30,11 +29,12 @@ def reconnect(self, client, info): self.connect_at = time.time() self.disconnect_at = None def mark_disconnected(self): + if self.disconnect_at is None: print(f"Tab disconnected: {self.url} (Session: {self.id})") self.disconnect_at = time.time() class TMWebDriver: - def __init__(self, host: str = 'localhost', port: int = 18765): + def __init__(self, host: str = '127.0.0.1', port: int = 18765): self.host, self.port = host, port self.sessions, self.results, self.acks = {}, {}, {} self.default_session_id = None @@ -68,7 +68,7 @@ def long_poll(): try: msg = msgQ.get(timeout=0.2) try: self.acks[json.loads(msg).get('id','')] = True - except: traceback.print_exc() + except Exception: traceback.print_exc() return msg except queue.Empty: continue return json.dumps({"id": "", "ret": "next long-poll"}) @@ -79,7 +79,7 @@ def result(): if data.get('type') == 'result': self.results[data.get('id')] = {'success': True, 'data': data.get('result'), 'newTabs': data.get('newTabs', [])} elif data.get('type') == 'error': - self.results[data.get('id')] = {'success': False, 'data': data.get('error')} + self.results[data.get('id')] = {'success': False, 'data': data.get('error'), 'newTabs': data.get('newTabs', [])} return 'ok' @app.route('/link', method=['GET','POST']) @@ -93,12 +93,11 @@ def link(): session_id = data.get('sessionId') code = data.get('code') timeout = float(data.get('timeout', 10.0)) - try: - result = self.execute_js(code, timeout=timeout, session_id=session_id) - print('[remote result]', (str(code)[:50] + ' RESULT:' +str(result)[:50]).replace('\n', ' ')) - return json.dumps({'r': result}, ensure_ascii=False) - except Exception as e: - return json.dumps({'error': str(e)}, ensure_ascii=False) + try: result = self.execute_js(code, timeout=timeout, session_id=session_id) + except Exception as e: return json.dumps({'r': {'error': str(e)}}, ensure_ascii=False) + try: print('[remote result]', (str(code)[:50] + ' RESULT:' +str(result)[:50]).replace('\n', ' ')) + except Exception: pass + return json.dumps({'r': result}, ensure_ascii=False) return 'ok' def run(): from wsgiref.simple_server import make_server, WSGIServer, WSGIRequestHandler @@ -128,16 +127,32 @@ def handle(self) -> None: session_info = {'url': data.get('url'), 'title': data.get('title', ''), 'connected_at': time.time(), 'type': 'ws'} driver._register_client(session_id, self, session_info) + elif data.get('type') in ['ext_ready', 'tabs_update']: + tabs = data.get('tabs', []) + current_tab_ids = {str(tab['id']) for tab in tabs} + print(f"Received tabs update: {current_tab_ids}") + for sid in list(driver.sessions.keys()): + sess = driver.sessions[sid] + if sess.type == 'ext_ws' and sid not in current_tab_ids: + sess.mark_disconnected() + for tab in tabs: + session_id = str(tab['id']) + session_info = {'url': tab.get('url'), 'title': tab.get('title', ''), 'connected_at': time.time(), 'type': 'ext_ws'} + sess = driver.sessions.get(session_id) + if sess and sess.is_active(): sess.info = session_info + else: driver._register_client(session_id, self, session_info) elif data.get('type') == 'ack': driver.acks[data.get('id','')] = True elif data.get('type') == 'result': driver.results[data.get('id')] = {'success': True, 'data': data.get('result'), 'newTabs': data.get('newTabs', [])} elif data.get('type') == 'error': - driver.results[data.get('id')] = {'success': False, 'data': data.get('error')} + driver.results[data.get('id')] = {'success': False, 'data': data.get('error'), 'newTabs': data.get('newTabs', [])} except Exception as e: print(f"Error handling message: {e}") if hasattr(self, 'data'): print(self.data) def connected(self): (f"New connection from {self.address}") - def handle_close(self): driver._unregister_client(self) + def handle_close(self): + print(f"WS Connection closed: {self.address}") + driver._unregister_client(self) self.server = WebSocketServer(self.host, self.port, JSExecutor) server_thread = threading.Thread(target=self.server.serve_forever) @@ -159,13 +174,10 @@ def _register_client(self, session_id: str, client: WebSocket, session_info) -> self.latest_session_id = session_id if self.default_session_id is None: self.default_session_id = session_id - def _unregister_client(self, client: WebSocket) -> None: for session in self.sessions.values(): - if session.ws_client == client: - session.mark_disconnected() - break + if session.ws_client == client: session.mark_disconnected() def execute_js(self, code, timeout=15, session_id=None) -> Any: if session_id is None: session_id = self.default_session_id @@ -190,11 +202,14 @@ def execute_js(self, code, timeout=15, session_id=None) -> Any: raise ValueError(f"会话ID {session_id} 未连接") tp = session.type - assert tp in ['ws', 'http'], f"Unsupported session type: {tp}" + if tp not in ('ws', 'http', 'ext_ws'): + raise ValueError(f"Unsupported session type: {tp}") exec_id = str(uuid.uuid4()) - payload = json.dumps({'id': exec_id, 'code': code}) + payload_dict = {'id': exec_id, 'code': code} + if tp == 'ext_ws': payload_dict['tabId'] = int(session.id) + payload = json.dumps(payload_dict) - if tp == 'ws': session.ws_client.send_message(payload) + if tp in ['ws', 'ext_ws']: session.ws_client.send_message(payload) elif tp == 'http': session.http_queue.put(payload) start_time = time.time() @@ -202,15 +217,15 @@ def execute_js(self, code, timeout=15, session_id=None) -> Any: hasjump = acked = False while exec_id not in self.results: - time.sleep(0.5) + time.sleep(0.2) if not acked and exec_id in self.acks: acked = True; start_time = time.time() - if tp == 'ws': + if tp in ['ws', 'ext_ws']: if not session.is_active(): hasjump = True if hasjump and session.is_active(): return {'result': f"Session {session_id} reloaded.", "closed":1} if time.time() - start_time > timeout: - if tp == 'ws': + if tp in ['ws', 'ext_ws']: if hasjump: return {'result': f"Session {session_id} reloaded and new page is loading...", 'closed':1} if acked: return {"result": f"No response data in {timeout}s (ACK received, script may still be running)"} return {"result": f"No response data in {timeout}s (no ACK, script may not have been delivered)"} @@ -227,7 +242,9 @@ def execute_js(self, code, timeout=15, session_id=None) -> Any: return rr def _remote_cmd(self, cmd): - return requests.post(self.remote, headers={"Content-Type": "application/json"}, json=cmd).json() + try: return requests.post(self.remote, headers={"Content-Type": "application/json"}, json=cmd, timeout=30).json() + except (ConnectionError, requests.exceptions.ConnectionError): + raise ConnectionError("TMWebDriver master未运行,看tmwebdriver_sop后台启动一个TMWebDriver") def get_all_sessions(self): if self.is_remote: @@ -261,9 +278,6 @@ def set_session(self, url_pattern: str) -> bool: return self.default_session_id def jump(self, url, timeout=10): self.execute_js(f"window.location.href='{url}'", timeout=timeout) - def newtab(self, url=None): - if url is None: url = "http://www.baidu.com/robots.txt" - return self.execute_js(f'GM_openInTab("{url}");') if __name__ == "__main__": - driver = TMWebDriver(host='localhost', port=18765) \ No newline at end of file + driver = TMWebDriver(host='127.0.0.1', port=18765) \ No newline at end of file diff --git a/WELCOME_NEW_USER.md b/WELCOME_NEW_USER.md deleted file mode 100644 index 18403527b..000000000 --- a/WELCOME_NEW_USER.md +++ /dev/null @@ -1,61 +0,0 @@ -# 🚀 欢迎使用物理级全能执行者 - -这是您的 Agent 初始化指引。请按以下阶段操作: - -## 第一阶段:环境启动 (Initial Ignition) - -1. **Python 环境检查与核心库安装** - - 确保安装了 Python 3.10+。 - - **必须手动执行**:`pip install streamlit pywebview` - -2. **配置身份密钥 (Credentials)** - - 复制 `mykey_template.py` 为 `mykey.py` 并填入 API Key。 - -3. **唤醒 Agent** - - 运行 `launch.pyw`。看到悬浮窗后,我即刻上线。 - -## 第二阶段:能力激活 (Ability Activation) - -在此阶段,您只需对我发送指令,所有物理操作由我完成: - -1. **解锁 PowerShell 脚本执行权限** - - **指令**:`请帮我当前用户解锁 powershell 的 ps1 执行权限。` - -2. **配置全局文件搜索 (Everything CLI)** - - **指令**:`安装并配置 everything 命令行工具进PATH。` - -3. **Web 自动化环境配置 (Web Setup SOP)** - - **指令**:`执行 web setup sop 解锁 web 工具` - - **物理影响**:我将引导您完成浏览器插件安装,并注入核心脚本,使我能够直接操控您的浏览器页面。 - -4. **补全常用工具库** - - **指令**:`安装常用 Python 自动化包(如 requests, pandas, pyperclip)。` - -5. **配置网络代理 (Proxy Setup)** - - **指令**:`告诉我能用的系统代理` - -6. **激活视觉理解能力 (OCR & Vision)** - - **指令**:`配置截图与 OCR 工具,解锁你的屏幕视觉。` - -7. **移动端自动化准备 (Android/ADB)** - - **指令**:`配置 ADB 环境,准备连接安卓设备。` - -## 第三阶段:记忆与知识体系建造 (Knowledge Architecture) - -当环境就绪后,您可以让我构建您的“数字大脑”: - -1. **自动化 SOP 沉淀** - - **指令**:`记录刚才的操作流程,生成一套自动化 SOP。` - -2. **现有技能挂载 (SOP Retrieval)** - - **指令**:`读取现有 SOP 目录,告诉我你现在掌握的所有技能。` - -3. **物理资产审计 (Asset Audit)** - - **指令**:`帮我建立物理资产清单,扫描并记录我常用的工具路径。` - -4. **Web 调研实战 (Research & Report)** - - **指令**:`搜索 [关键词],并根据网页内容整理一份简易 Markdown 报告保存到当前目录。` - - **物理影响**:我将自动打开浏览器,利用 Web 驱动采集多个页面信息,通过逻辑整合后在您的本地文件夹生成物理文件。 - ---- -**💡 提示**:您可以直接复制上述 `指令` 发送给我,我将立刻执行对应的物理操作。 diff --git a/agent_loop.py b/agent_loop.py index 25200e94e..3574e8e4b 100644 --- a/agent_loop.py +++ b/agent_loop.py @@ -1,38 +1,34 @@ -import json, re +import json, re, os from dataclasses import dataclass from typing import Any, Optional +try: from plugins.hooks import trigger as _hook +except ImportError: _hook = lambda *a, **k: None @dataclass class StepOutcome: data: Any next_prompt: Optional[str] = None should_exit: bool = False - def try_call_generator(func, *args, **kwargs): ret = func(*args, **kwargs) - if hasattr(ret, '__iter__') and not isinstance(ret, (str, bytes, dict, list)): - ret = yield from ret + if hasattr(ret, '__iter__') and not isinstance(ret, (str, bytes, dict, list)): ret = yield from ret return ret class BaseHandler: - def tool_before_callback(self, tool_name, args, response): pass - def tool_after_callback(self, tool_name, args, response, ret): pass - def dispatch(self, tool_name, args, response): + def turn_end_callback(self, response, tool_calls, tool_results, turn, next_prompt, exit_reason): return next_prompt + def dispatch(self, tool_name, args, response, index=0, tool_num=1): method_name = f"do_{tool_name}" if hasattr(self, method_name): - _ = yield from try_call_generator(self.tool_before_callback, tool_name, args, response) + args['_index'] = index; args['_tool_num'] = tool_num + _hook('tool_before', locals()) ret = yield from try_call_generator(getattr(self, method_name), args, response) - _ = yield from try_call_generator(self.tool_after_callback, tool_name, args, response, ret) + _hook('tool_after', locals()) return ret - elif tool_name == 'bad_json': - return StepOutcome(None, next_prompt=args.get('msg', 'bad_json'), should_exit=False) + elif tool_name == 'bad_json': return StepOutcome(None, next_prompt=args.get('msg', 'bad_json'), should_exit=False) else: yield f"未知工具: {tool_name}\n" return StepOutcome(None, next_prompt=f"未知工具 {tool_name}", should_exit=False) -def json_default(o): - if isinstance(o, set): return list(o) - return str(o) - +def json_default(o): return list(o) if isinstance(o, set) else str(o) def exhaust(g): try: while True: next(g) @@ -40,54 +36,98 @@ def exhaust(g): def get_pretty_json(data): if isinstance(data, dict) and "script" in data: - data = data.copy() - data["script"] = data["script"].replace("; ", ";\n ") + data = data.copy(); data["script"] = data["script"].replace("; ", ";\n ") return json.dumps(data, indent=2, ensure_ascii=False).replace('\\n', '\n') -def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, max_turns=15, verbose=True): +def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, + max_turns=40, verbose=True, initial_user_content=None, yield_info=False): messages = [ {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_input} + {"role": "user", "content": initial_user_content if initial_user_content is not None else user_input} ] - for turn in range(max_turns): - yield f"**LLM Running (Turn {turn+1}) ...**\n\n" - if (turn+1) % 10 == 0: client.last_tools = '' # 每10轮重置一次工具描述,避免上下文过大导致的模型性能下降 + turn = 0; handler.max_turns = max_turns + _hook('agent_before', locals()) + while turn < handler.max_turns: + turn += 1; turnstr = f'LLM Running (Turn {turn}) ...' + if handler.parent.task_dir: turnstr = f'Turn {turn} ...' + if verbose: turnstr = f'**{turnstr}**' + if yield_info: yield {'turn': turn} + yield f"\n{turnstr}\n\n" + if turn%10 == 0: client.last_tools = '' # 每10轮重置一次工具描述 + _hook('turn_before', locals()) + _hook('llm_before', locals()) response_gen = client.chat(messages=messages, tools=tools_schema) - response = yield from response_gen - if verbose: yield '\n\n' - - if not response.tool_calls: - tool_name, args = 'no_tool', {} - else: - tool_call = response.tool_calls[0] - tool_name = tool_call.function.name - args = json.loads(tool_call.function.arguments) - - if tool_name == 'no_tool': pass - else: - showarg = get_pretty_json(args) - if not verbose and len(showarg) > 200: showarg = showarg[:200] + ' ...' - yield f"🛠️ **正在调用工具:** `{tool_name}` 📥**参数:**\n````text\n{showarg}\n````\n" - gen = handler.dispatch(tool_name, args, response) if verbose: - yield '`````\n' - outcome = yield from gen - yield '`````\n' + response = yield from response_gen + yield '\n\n' else: - outcome = exhaust(gen) + response = exhaust(response_gen) + cleaned = _clean_content(response.content) + if cleaned: yield cleaned + '\n' + _hook('llm_after', locals()) + + if not response.tool_calls: tool_calls = [{'tool_name': 'no_tool', 'args': {}}] + else: tool_calls = [{'tool_name': tc.function.name, 'args': json.loads(tc.function.arguments), 'id': tc.id} + for tc in response.tool_calls] + + tool_results = []; next_prompts = set(); exit_reason = {} + for ii, tc in enumerate(tool_calls): + tool_name, args, tid = tc['tool_name'], tc['args'], tc.get('id', '') + if tool_name == 'no_tool': pass + else: + if verbose: yield f"🛠️ Tool: `{tool_name}` 📥 args:\n````text\n{get_pretty_json(args)}\n````\n" + else: yield f"🛠️ {tool_name}({_compact_tool_args(tool_name, args)})\n" + handler.current_turn = turn + gen = handler.dispatch(tool_name, args, response, index=ii, tool_num=len(tool_calls)) + try: + v = next(gen) + def proxy(): yield v; return (yield from gen) + if verbose: yield '`````\n' + outcome = (yield from proxy()) if verbose else exhaust(proxy()) + if verbose: yield '`````\n' + except StopIteration as e: outcome = e.value + + if outcome.should_exit: + exit_reason = {'result': 'EXITED', 'data': outcome.data}; break + if not outcome.next_prompt: + exit_reason = {'result': 'CURRENT_TASK_DONE', 'data': outcome.data}; break + if outcome.next_prompt.startswith('未知工具'): client.last_tools = '' + if outcome.data is not None and tool_name != 'no_tool': + datastr = json.dumps(outcome.data, ensure_ascii=False, default=json_default) if type(outcome.data) in [dict, list] else str(outcome.data) + tool_results.append({'tool_use_id': tid, 'content': datastr}) + next_prompts.add(outcome.next_prompt) + if len(next_prompts) == 0 or exit_reason: + if len(handler._done_hooks) == 0 or exit_reason.get('result', '') == 'EXITED': break + next_prompts.add(handler._done_hooks.pop(0)) + next_prompt = handler.turn_end_callback(response, tool_calls, tool_results, turn, '\n'.join(next_prompts), exit_reason) + _hook('turn_after', locals()) + messages = [{"role": "user", "content": next_prompt, "tool_results": tool_results}] # just new message, history is kept in *Session + if exit_reason: handler.turn_end_callback(response, tool_calls, tool_results, turn, '', exit_reason) + _hook('agent_after', locals()) + return exit_reason or {'result': 'MAX_TURNS_EXCEEDED'} - if outcome.next_prompt is None: return {'result': 'CURRENT_TASK_DONE', 'data': outcome.data} - if outcome.should_exit: return {'result': 'EXITED', 'data': outcome.data} - if outcome.next_prompt.startswith('未知工具'): client.last_tools = '' +def _clean_content(text): + if not text: return '' + def _shrink_code(m): + lines = m.group(0).split('\n') + lang = lines[0].replace('```','').strip() + body = [l for l in lines[1:-1] if l.strip()] + if len(body) <= 6: return m.group(0) + preview = '\n'.join(body[:5]) + return f'```{lang}\n{preview}\n ... ({len(body)} lines)\n```' + text = re.sub(r'```[\s\S]*?```', _shrink_code, text) + for p in [r'[\s\S]*?', r'[\s\S]*?', r'(\r?\n){3,}']: + text = re.sub(p, '\n\n' if '\\n' in p else '', text) + return text.strip() - next_prompt = "" - if outcome.data is not None: - datastr = json.dumps(outcome.data, ensure_ascii=False, default=json_default) if type(outcome.data) in [dict, list] else str(outcome.data) - next_prompt += f"\n{datastr}\n\n\n" - next_prompt += outcome.next_prompt - if (turn+1) % 7 == 0: - next_prompt += f"\n\n[DANGER] 已连续执行第 {turn+1} 轮。禁止无效重试。若无有效进展,必须切换策略:1. 探测物理边界 2. 请求用户协助。" - if (turn+1) % 30 == 0: - next_prompt += f"\n\n### [DANGER] 已连续执行第 {turn+1} 轮。你必须总结情况进行ask_user,不允许继续重试。" - messages = [{"role": "user", "content": next_prompt}] - return {'result': 'MAX_TURNS_EXCEEDED'} \ No newline at end of file +def _compact_tool_args(name, args): + a = {k: v for k, v in args.items() if k != '_index'} + for k in ('path',): + if k in a: a[k] = os.path.basename(a[k]) + if name == 'update_working_checkpoint': s = a.get('key_info', ''); return (s[:60]+'...') if len(s)>60 else s + if name == 'ask_user': + q = str(a.get('question', '')) + cs = a.get('candidates') or [] + if cs: q += '\ncandidates:\n' + '\n'.join(f'- {c}' for c in cs) + return q + s = json.dumps(a, ensure_ascii=False); return (s[:120]+'...') if len(s)>120 else s diff --git a/agentmain.py b/agentmain.py index e3dd6bddb..d58b7d0d7 100644 --- a/agentmain.py +++ b/agentmain.py @@ -1,162 +1,316 @@ -import os, sys, threading, queue, time, json, re, random +import os, sys, threading, queue, time, json, re, random, locale, glob +os.environ.setdefault('GA_LANG', 'zh' if any(k in (locale.getlocale()[0] or '').lower() for k in ('zh', 'chinese')) else 'en') if sys.stdout is None: sys.stdout = open(os.devnull, "w") elif hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(errors='replace') if sys.stderr is None: sys.stderr = open(os.devnull, "w") elif hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(errors='replace') sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from sidercall import SiderLLMSession, LLMSession, ToolClient, ClaudeSession, XaiSession -from agent_loop import agent_runner_loop, StepOutcome, BaseHandler -from ga import GenericAgentHandler, smart_format, get_global_memory, format_error +from llmcore import reload_mykeys, ToolClient, MixinSession, NativeToolClient, NativeClaudeSession, NativeOAISession, resolve_client +from agent_loop import agent_runner_loop +try: + from plugins.hooks import discover_and_load; discover_and_load() +except Exception: pass +from ga import GenericAgentHandler, smart_format, get_global_memory, format_error, consume_file -with open('assets/tools_schema.json', 'r', encoding='utf-8') as f: - TS = f.read() +script_dir = os.path.dirname(os.path.abspath(__file__)) +BANNED_TOOLS = (['ask_user', 'start_long_term_update'] if '--no-user-tools' in sys.argv else []) +def load_tool_schema(suffix=''): + global TOOLS_SCHEMA + TS = open(os.path.join(script_dir, f'assets/tools_schema{suffix}.json'), 'r', encoding='utf-8').read() TOOLS_SCHEMA = json.loads(TS if os.name == 'nt' else TS.replace('powershell', 'bash')) + TOOLS_SCHEMA = [t for t in TOOLS_SCHEMA if t.get('function', {}).get('name') not in BANNED_TOOLS] +load_tool_schema() + +lang_suffix = '_en' if os.environ.get('GA_LANG', '') == 'en' else '' +mem_dir = os.path.join(script_dir, 'memory') +if not os.path.exists(mem_dir): os.makedirs(mem_dir) +mem_txt = os.path.join(mem_dir, 'global_mem.txt') +if not os.path.exists(mem_txt): open(mem_txt, 'w', encoding='utf-8').write('# [Global Memory - L2]\n') +mem_insight = os.path.join(mem_dir, 'global_mem_insight.txt') +if not os.path.exists(mem_insight): + t = os.path.join(script_dir, f'assets/global_mem_insight_template{lang_suffix}.txt') + open(mem_insight, 'w', encoding='utf-8').write(open(t, encoding='utf-8').read() if os.path.exists(t) else '') +cdp_cfg = os.path.join(script_dir, 'assets/tmwd_cdp_bridge/config.js') +if not os.path.exists(cdp_cfg): + try: + os.makedirs(os.path.dirname(cdp_cfg), exist_ok=True) + open(cdp_cfg, 'w', encoding='utf-8').write(f"const TID = '__ljq_{hex(random.randint(0, 99999999))[2:8]}';") + except Exception as e: print(f'[WARN] CDP config init failed: {e} — advanced web features (tmwebdriver) will be unavailable.') def get_system_prompt(): - if not os.path.exists('memory'): os.makedirs('memory') - if not os.path.exists('memory/global_mem.txt'): - with open('memory/global_mem.txt', 'w', encoding='utf-8') as f: f.write('') - if not os.path.exists('memory/global_mem_insight.txt'): - t = 'assets/global_mem_insight_template.txt' - open('memory/global_mem_insight.txt', 'w', encoding='utf-8').write(open(t, encoding='utf-8').read() if os.path.exists(t) else '') - with open('assets/sys_prompt.txt', 'r', encoding='utf-8') as f: prompt = f.read() + with open(os.path.join(script_dir, f'assets/sys_prompt{lang_suffix}.txt'), 'r', encoding='utf-8') as f: prompt = f.read() prompt += f"\nToday: {time.strftime('%Y-%m-%d %a')}\n" prompt += get_global_memory() return prompt -class GeneraticAgent: +# SDK: +# agent = GenericAgent(); threading.Thread(target=agent.run, daemon=True).start() +# output1_queue = agent.put_task(prompt1) +# output2_queue = agent.put_task(prompt2) +class GenericAgent: def __init__(self): - if not os.path.exists('temp'): os.makedirs('temp') - from sidercall import mykeys + os.makedirs(os.path.join(script_dir, 'temp'), exist_ok=True) + self.lock = threading.Lock() + self.task_dir = None + self.history = []; self.handler = None; + self.task_queue = queue.Queue() + self.is_running = False; self.stop_sig = False; self.llm_no = 0; + self.inc_out = False; self.verbose = True + self.peer_hint = True + self.force_non_stream = False + logid = f'{(time.time_ns() + random.randrange(1_000_000)) % 1_000_000:06d}' + self.log_path = os.path.join(script_dir, f'temp/model_responses/model_responses_{logid}.txt') + self.load_llm_sessions() + self.extra_sys_prompts = [] + self.intervene = self.extrakeyinfo = None + + def load_llm_sessions(self): + mykeys, changed = reload_mykeys() + if not changed and hasattr(self, 'llmclients'): return + try: oldhistory = self.llmclient.backend.history + except: oldhistory = None llm_sessions = [] for k, cfg in mykeys.items(): if not any(x in k for x in ['api', 'config', 'cookie']): continue try: - if 'claude' in k: llm_sessions += [ClaudeSession(api_key=cfg['apikey'], api_base=cfg['apibase'], model=cfg['model'])] - if 'oai' in k: llm_sessions += [LLMSession(api_key=cfg['apikey'], api_base=cfg['apibase'], model=cfg['model'], proxy=cfg.get('proxy'))] - if 'xai' in k: llm_sessions += [XaiSession(cfg, mykeys.get('proxy', ''))] - if 'sider' in k: llm_sessions += [SiderLLMSession(cfg, default_model=x) for x in \ - ["gemini-3.0-flash", "claude-haiku-4.5", "kimi-k2"]] + if 'mixin' in k: llm_sessions += [{'mixin_cfg': cfg}] + elif c := resolve_client(k): llm_sessions += [c] except: pass - if len(llm_sessions) > 0: self.llmclient = ToolClient(llm_sessions, auto_save_tokens=True) - else: self.llmclient = None - self.lock = threading.Lock() - self.history = [] - self.task_queue = queue.Queue() - self.is_running, self.stop_sig = False, False - self.llm_no = 0 - self.inc_out = False - self.handler = None - self.verbose = True - + for i, s in enumerate(llm_sessions): + if isinstance(s, dict) and 'mixin_cfg' in s: + try: + mixin = MixinSession(llm_sessions, s['mixin_cfg']) + if isinstance(mixin._sessions[0], (NativeClaudeSession, NativeOAISession)): llm_sessions[i] = NativeToolClient(mixin) + else: llm_sessions[i] = ToolClient(mixin) + except Exception as e: print(f'\n\n\n[ERROR] Failed to init MixinSession with cfg {s["mixin_cfg"]}: {e}!!!\n\n') + self.llmclients = llm_sessions + self.llmclient = self.llmclients[self.llm_no%len(self.llmclients)] + if oldhistory: self.llmclient.backend.history = oldhistory + def next_llm(self, n=-1): - self.llm_no = ((self.llm_no + 1) if n < 0 else n) % len(self.llmclient.backends) + self.load_llm_sessions() + self.llm_no = ((self.llm_no + 1) if n < 0 else n) % len(self.llmclients) + lastc = self.llmclient + self.llmclient = self.llmclients[self.llm_no] + try: self.llmclient.backend.history = lastc.backend.history + except: raise Exception('[ERROR] BAD Mixin config: Check your mykey.py') self.llmclient.last_tools = '' - def list_llms(self): return [(i, f"{type(b).__name__}/{b.default_model}", i == self.llm_no) for i, b in enumerate(self.llmclient.backends)] - def get_llm_name(self): - b = self.llmclient.backends[self.llm_no] - return f"{type(b).__name__}/{b.default_model}" + name = self.get_llm_name(model=True) + if 'glm' in name or 'minimax' in name or 'kimi' in name: load_tool_schema('_cn') + else: load_tool_schema() + def list_llms(self): + self.load_llm_sessions() + return [(i, self.get_llm_name(b), i == self.llm_no) for i, b in enumerate(self.llmclients)] + def get_llm_name(self, b=None, model=False): + b = self.llmclient if b is None else b + if isinstance(b, dict): return 'BADCONFIG_MIXIN' + if model: return b.backend.model.lower() + return f"{type(b.backend).__name__}/{b.backend.name}" + def get_ctx_multiplier(self): return getattr(self.llmclient.backend, 'maxlen_multiplier', 1.0) def abort(self): - print('Abort current task...') if not self.is_running: return + print('Abort current task...') self.stop_sig = True - if self.handler is not None: - self.handler.code_stop_signal.append(1) + if self.handler is not None: self.handler.code_stop_signal.append(1) - def put_task(self, query, source="user"): + def put_task(self, query, source="user", images=None): display_queue = queue.Queue() - self.task_queue.put({"query": query, "source": source, "output": display_queue}) + self.task_queue.put({"query": query, "source": source, "images": images or [], "output": display_queue}) return display_queue + # i know it is dangerous, but raw_query is dangerous enough it doesn't enlarge + def _handle_slash_cmd(self, raw_query, display_queue): + if not raw_query.startswith('/'): return raw_query + if _sm := re.match(r'/session\.(\w+)=(.*)', raw_query.strip()): + k, v = _sm.group(1), _sm.group(2) + vfile = os.path.join(script_dir, 'temp', v) + if os.path.isfile(vfile): v = open(vfile, encoding='utf-8').read().strip() + try: v = json.loads(v) # cover number parsing + except (json.JSONDecodeError, ValueError): pass + setattr(self.llmclient.backend, k, v) + display_queue.put({'done': smart_format(f"✅ session.{k} = {repr(v)}", max_str_len=500), 'source': 'system'}) + return None + if raw_query.strip() == '/resume': + return r'帮我看看最近有哪些会话可以恢复。读model_responses/目录,按修改时间取最近10个文件,从每个文件里找最后一个...块,用一句话总结每个会话在聊什么,列表给我选。注意读文件后要把字面的\n替换成真换行才能正确匹配。' + return raw_query + def run(self): while True: task = self.task_queue.get() - self.is_running = True + if isinstance(task, str): break raw_query, source, display_queue = task["query"], task["source"], task["output"] + raw_query = self._handle_slash_cmd(raw_query, display_queue) + if raw_query is None: + self.task_queue.task_done(); continue + self.is_running = True + if len(raw_query) > 2000: + task_file = os.path.join(script_dir, 'temp', f'user_prompt_{int(time.time())}.md') + with open(task_file, 'w', encoding='utf-8') as f: f.write(raw_query) + raw_query = f'Long user prompt saved to {task_file}. Read and execute.' rquery = smart_format(raw_query.replace('\n', ' '), max_str_len=200) self.history.append(f"[USER]: {rquery}") - - sys_prompt = get_system_prompt() - handler = GenericAgentHandler(None, self.history, './temp') - if self.handler and self.handler.key_info: - handler.key_info = self.handler.key_info - if '清除工作记忆' not in handler.key_info: - handler.key_info += '\n[SYSTEM] 如果是新任务,请先更新或清除工作记忆\n' - self.handler = handler - self.llmclient.backend = self.llmclient.backends[self.llm_no] - gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, - handler, TOOLS_SCHEMA, max_turns=40, verbose=self.verbose) + sys_prompt = get_system_prompt() + '\n'.join(self.extra_sys_prompts) + getattr(self.llmclient.backend, 'extra_sys_prompt', '') + if self.peer_hint: sys_prompt += f"\n[Peer] 用户提及其他会话/后台任务状态时: temp/model_responses/ (只找近期修改的文件尾部)\n" + handler = GenericAgentHandler(self, self.history, os.path.join(script_dir, 'temp')) + if getattr(self, 'no_print', False): handler.print = lambda *a, **k: None + if self.handler and 'key_info' in self.handler.working: + ki = re.sub(r'\n\[SYSTEM\] 此为.*?工作记忆[。\n]*', '', self.handler.working['key_info']) # 去旧 + handler.working['key_info'] = ki + handler.working['passed_sessions'] = ps = self.handler.working.get('passed_sessions', 0) + 1 + if ps > 0: handler.working['key_info'] += f'\n[SYSTEM] 此为 {ps} 个对话前设置的key_info,若已在新任务,先更新或清除工作记忆。\n' + self.handler = handler # although new handler, the **full** history is in llmclient, so it is full history! + self.llmclient.log_path = self.log_path + if self.force_non_stream: + self.llmclient.backend.stream = False + self.llmclient.backend.read_timeout = max(self.llmclient.backend.read_timeout, 1200) + gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, handler, TOOLS_SCHEMA, + max_turns=180, verbose=self.verbose, yield_info=True) try: - full_resp = ""; last_pos = 0 + full_resp = ""; last_pos = 0; curr_turn = 0; turn_resps = [] for chunk in gen: + if consume_file(self.task_dir, '_stop'): self.abort() if self.stop_sig: break - full_resp += chunk - if len(full_resp) - last_pos > 50: - display_queue.put({'next': full_resp[last_pos:] if self.inc_out else full_resp, 'source': source}) + if isinstance(chunk, dict) and 'turn' in chunk: + curr_turn = chunk['turn']; turn_resps.append(''); continue + full_resp += chunk; turn_resps[-1] += chunk + if len(full_resp) - last_pos > 30 or 'LLM Running' in chunk: + display_queue.put({'next': full_resp[last_pos:] if self.inc_out else full_resp, + 'source': source, 'turn': curr_turn, 'outputs': turn_resps[-2:]}) last_pos = len(full_resp) - if self.inc_out and last_pos < len(full_resp): display_queue.put({'next': full_resp[last_pos:], 'source': source}) - if '' in full_resp: full_resp = full_resp.replace('', '\n\n') - if '' in full_resp: full_resp = re.sub(r'\s*(.*?)\s*', r'\n````\n\n\1\n\n````', full_resp, flags=re.DOTALL) - display_queue.put({'done': full_resp, 'source': source}) + if self.inc_out and last_pos < len(full_resp): + display_queue.put({'next': full_resp[last_pos:], 'source': source, + 'turn': curr_turn, 'outputs': turn_resps[-2:]}) + display_queue.put({'done': full_resp, 'source': source, 'turn': curr_turn, 'outputs': turn_resps.copy()}) self.history = handler.history_info except Exception as e: print(f"Backend Error: {format_error(e)}") - display_queue.put({'done': full_resp + f'\n```\n{format_error(e)}\n```', 'source': source}) + display_queue.put({'done': full_resp + f'\n```\n{format_error(e)}\n```', 'source': source, 'turn': curr_turn, 'outputs': turn_resps.copy()}) finally: + if self.stop_sig: print('User aborted the task.') self.is_running = self.stop_sig = False self.task_queue.task_done() if self.handler is not None: self.handler.code_stop_signal.append(1) - +GeneraticAgent = GenericAgent + if __name__ == '__main__': import argparse from datetime import datetime parser = argparse.ArgumentParser() - parser.add_argument('--scheduled', action='store_true', help='计划任务轮询模式') - parser.add_argument('--task', metavar='IODIR', help='一次性任务模式(文件IO)') - parser.add_argument('--llm_no', type=int, default=0, help='LLM编号') - args = parser.parse_args() + parser.add_argument('--task', metavar='IODIR', help='一次性任务模式,先看subagent.md') + parser.add_argument('--func', metavar='PROMPT_FILE', help='纯函数模式:读prompt文件→结果写prompt.out.txt→退出') + parser.add_argument('--reflect', metavar='SCRIPT', help='反射模式:加载监控脚本,check()触发时发任务') + parser.add_argument('--input', help='prompt') + parser.add_argument('--history', help='history json file') + parser.add_argument('--llm_no', type=int, default=0) + parser.add_argument('--verbose', action='store_true') + parser.add_argument('--nobg', action='store_true') + parser.add_argument('--nolog', action='store_true') + parser.add_argument('--no-user-tools', action='store_true') + args, _unknown = parser.parse_known_args() + _extra_args = dict(zip([k.lstrip('-') for k in _unknown[::2]], _unknown[1::2])) if _unknown else {} + + if (args.func or args.task) and not args.nobg: + import subprocess, platform + cmd = [sys.executable, os.path.abspath(__file__)] + [a for a in sys.argv[1:]] + ['--nobg'] + if args.task: + d = os.path.join(script_dir, f'temp/{args.task}'); os.makedirs(d, exist_ok=True) + out = open(os.path.join(d, 'stdout.log'), 'w', encoding='utf-8') + err = open(os.path.join(d, 'stderr.log'), 'w', encoding='utf-8') + else: out, err = subprocess.DEVNULL, subprocess.DEVNULL + p = subprocess.Popen(cmd, cwd=script_dir, + creationflags=0x08000000 if platform.system() == 'Windows' else 0, + stdout=out, stderr=err) + print('PID:', p.pid); sys.exit(0) - agent = GeneraticAgent() - agent.llm_no = args.llm_no - agent.verbose = False + agent = GenericAgent() + if args.nolog: agent.log_path = False + agent.next_llm(args.llm_no) + agent.verbose = args.verbose threading.Thread(target=agent.run, daemon=True).start() + histfile = args.history if args.task: - d = f'temp/{args.task}'; rp = f'{d}/reply.txt'; nround = '' - with open(f'{d}/input.txt', encoding='utf-8') as f: raw = f.read() + agent.task_dir = d = os.path.join(script_dir, f'temp/{args.task}'); nround = '' + infile = os.path.join(d, 'input.txt'); outfile = f'{d}/output{nround}.txt' + if args.input: + os.makedirs(d, exist_ok=True) + [os.remove(f) for f in glob.glob(os.path.join(d, 'output*.txt'))] + with open(infile, 'w', encoding='utf-8') as f: f.write(args.input) + histfile = histfile or os.path.join(d, '_history.json') + elif args.func: + infile = args.func; outfile = os.path.splitext(args.func)[0] + '.out.txt' + + if histfile and os.path.isfile(histfile): agent.llmclient.backend.history = json.loads(open(histfile, encoding='utf-8').read()) + + if args.func or args.task: + agent.peer_hint = False + with open(infile, encoding='utf-8') as f: raw = f.read() while True: - dq = agent.put_task(raw, source='task') - while 'done' not in (item := dq.get(timeout=120)): - if 'next' in item and random.random() < 0.05: # 1/20的概率写一次中间结果 - with open(f'{d}/output{nround}.txt', 'w', encoding='utf-8') as f: f.write(item.get('next', '')) - with open(f'{d}/output{nround}.txt', 'w', encoding='utf-8') as f: f.write(item['done'] + '\n[ROUND END]\n') - for _ in range(150): # 等reply.txt,5分钟超时 + dq = agent.put_task(raw, source='func' if args.func else 'task') + while 'done' not in (item := dq.get(timeout=2200)): + if 'next' in item: + with open(outfile, 'w', encoding='utf-8') as f: f.write(item.get('next', '')) + with open(outfile, 'w', encoding='utf-8') as f: f.write(item['done'] + '\n\n[ROUND END]\n') + if not args.task: break + consume_file(d, '_stop') # 已经成功停下来了,避免打断下次reply + for _ in range(300): # 等reply.txt,10分钟超时 time.sleep(2) - if os.path.exists(rp): - with open(rp, encoding='utf-8') as f: raw = f.read() - os.remove(rp); break + if (raw := consume_file(d, 'reply.txt')): break else: break - nround = int(nround) + 1 if nround.isdigit() else 1 - elif args.scheduled: - def drain(dq, tag): - while 'done' not in (item := dq.get()): pass - open('./temp/scheduler.log', 'a', encoding='utf-8').write(f'[{datetime.now():%m-%d %H:%M}] {tag}\n{item["done"]}\n\n') + nround = nround + 1 if isinstance(nround, int) else 1 + outfile = f'{d}/output{nround}.txt' + elif args.reflect: + agent.peer_hint = False + import importlib.util + spec = importlib.util.spec_from_file_location('reflect_script', args.reflect) + mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod) + if hasattr(mod, 'init'): mod.init(_extra_args) + _mt = os.path.getmtime(args.reflect) + print(f'[Reflect] loaded {args.reflect}' + (f' args={_extra_args}' if _extra_args else '')) while True: - time.sleep(55 + random.random() * 10) - now = datetime.now() - if not os.path.isdir('./sche_tasks/pending'): continue - for f in os.listdir('./sche_tasks/pending'): - m = re.match(r'(\d{4}-\d{2}-\d{2})_(\d{4})_', f) - if m and now >= datetime.strptime(f'{m[1]} {m[2]}', '%Y-%m-%d %H%M'): - raw = open(f'./sche_tasks/pending/{f}', encoding='utf-8').read() - dq = agent.put_task(f'按scheduled_task_sop执行任务文件 ../sche_tasks/pending/{f}(立刻移到running)\n内容:\n{raw}', source='scheduler') - threading.Thread(target=drain, args=(dq, f), daemon=True).start() - break + if os.path.getmtime(args.reflect) != _mt: + try: + spec.loader.exec_module(mod); _mt = os.path.getmtime(args.reflect) + if hasattr(mod, 'init'): mod.init(_extra_args) + print('[Reflect] reloaded') + except Exception as e: print(f'[Reflect] reload error: {e}') + try: task = mod.check() + except Exception as e: + print(f'[Reflect] check() error: {e}'); task = None + if task and task == '/exit': break + if task: + print(f'[Reflect] triggered: {task[:80]}') + dq = agent.put_task(task, source='reflect') + try: + while 'done' not in (item := dq.get(timeout=2200)): pass + result = item['done'] + print(result) + except Exception as e: + if getattr(mod, 'ONCE', False): raise + print(f'[Reflect] drain error: {e}'); result = f'[ERROR] {e}' + log_dir = os.path.join(script_dir, 'temp/reflect_logs'); os.makedirs(log_dir, exist_ok=True) + script_name = os.path.splitext(os.path.basename(args.reflect))[0] + open(os.path.join(log_dir, f'{script_name}_{datetime.now():%Y-%m-%d}.log'), 'a', encoding='utf-8').write(f'[{datetime.now():%m-%d %H:%M}]\n{result}\n\n') + if (on_done := getattr(mod, 'on_done', None)): + try: on_done(result) + except Exception as e: print(f'[Reflect] on_done error: {e}') + if getattr(mod, 'ONCE', False): print('[Reflect] ONCE=True, exiting.'); break + time.sleep(getattr(mod, 'INTERVAL', 5)) else: + try: import readline + except Exception: pass agent.inc_out = True + if sys.stdout.isatty(): + try: model = agent.get_llm_name(model=True) or '?' + except Exception: model = '?' + try: + sys.stdout.write(f'\x1b[92m✦\x1b[0m \x1b[1mGenericAgent\x1b[0m ' + f'\x1b[90m· cli · model:\x1b[0m {model}\n') + sys.stdout.flush() + except Exception: pass while True: q = input('> ').strip() if not q: continue @@ -167,5 +321,4 @@ def drain(dq, tag): if 'next' in item: print(item['next'], end='', flush=True) if 'done' in item: print(); break except KeyboardInterrupt: - agent.abort() - print('\n[Interrupted]') \ No newline at end of file + agent.abort(); print('\n[Interrupted]') diff --git a/assets/GenericAgent_Technical_Report.pdf b/assets/GenericAgent_Technical_Report.pdf new file mode 100644 index 000000000..cd5d59be6 Binary files /dev/null and b/assets/GenericAgent_Technical_Report.pdf differ diff --git a/assets/agent_bbs.py b/assets/agent_bbs.py new file mode 100644 index 000000000..7c8dd3548 --- /dev/null +++ b/assets/agent_bbs.py @@ -0,0 +1,225 @@ +# agent_bbs.py — 极简Agent公告板(多板块版) +# 启动: uvicorn agent_bbs:app --host 0.0.0.0 --port 58800 +# 或: python agent_bbs.py + +import sqlite3, uuid, time, json, os +from threading import Lock, Thread +from fastapi import FastAPI, HTTPException, Query, Body, UploadFile, File +from fastapi.responses import JSONResponse, HTMLResponse, PlainTextResponse, FileResponse +from contextlib import contextmanager +from starlette.requests import Request +from starlette.responses import Response +from starlette.middleware.base import BaseHTTPMiddleware + +# key → board config; 修改 boards.json 可热重载新增板块 +BOARDS_FILE = "boards.json" +DEFAULT_BOARDS = {"agent-bbs-test": {"name": "default", "db": "agent_bbs.db"}} +BOARDS, BOARDS_MTIME_NS, BOARDS_LOCK = DEFAULT_BOARDS, None, Lock() +_T=[time.time()] + +def load_boards_if_changed(): + global BOARDS, BOARDS_MTIME_NS + with BOARDS_LOCK: + if BOARDS_FILE is None: + if BOARDS_MTIME_NS is None: init_db(); BOARDS_MTIME_NS = 0 + return BOARDS + if not os.path.exists(BOARDS_FILE): + json.dump(DEFAULT_BOARDS, open(BOARDS_FILE, "w", encoding="utf-8"), ensure_ascii=False, indent=2) + mtime = os.stat(BOARDS_FILE).st_mtime_ns + if mtime == BOARDS_MTIME_NS: return BOARDS + try: + new = json.load(open(BOARDS_FILE, "r", encoding="utf-8")) + assert isinstance(new, dict) and all(isinstance(v, dict) and "db" in v and "name" in v for v in new.values()) + BOARDS, BOARDS_MTIME_NS = new, mtime; init_db() + print(f"[boards] reloaded {len(BOARDS)} boards") + except Exception as e: print(f"[boards] reload failed, keep old config: {e}") + return BOARDS + +UPLOAD_DIR = "bbs_files" + +app = FastAPI(title="Agent BBS", docs_url=None, redoc_url=None, openapi_url=None) + +class ApiKeyMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + key = request.headers.get("x-api-key") or request.query_params.get("key") + board = load_boards_if_changed().get(key) + if not board: return Response("Not Found", status_code=404) + request.state.board = board + return await call_next(request) + +app.add_middleware(ApiKeyMiddleware) + +HTML_PAGE = """ +Agent BBS + +

Agent BBS

+
+ + + + +
+
+""" + +README_TEXT = "Agent BBS API\tAuth: ALL requests require header X-API-Key: or pass ?key= as query parameter.\t1. Register: POST /register body: {\"name\": \"your-agent-name\"}\tResponse: {\"token\": \"xxx\", \"name\": \"your-agent-name\"}\t2. Post: POST /post body: {\"token\": \"xxx\", \"content\": \"your message\"}\tResponse: {\"id\": 1, \"author\": \"your-agent-name\"}\t3. Poll new: GET /poll?since_id=0&limit=50\tReturns posts with id > since_id, ordered by id asc. Keep track of the last id you received, use it as since_id next time.\t4. Query: GET /posts?author=xxx&limit=50\tauthor is optional. Returns posts ordered by id desc. 5. Upload file: POST /file/upload multipart/form-data, form fields: token (your agent token) + file (the file). Requires X-API-Key. Response: {\"ref\": \"a1b2c3/filename.ext\"}. Paste ref into post content to reference the file. 6. Download file: GET /file/{rand_id}/{filename} Requires X-API-Key. e.g. /file/a1b2c3/filename.ext" + +@app.get("/readme") +def readme(): return PlainTextResponse(README_TEXT) + +@app.get("/", response_class=HTMLResponse) +def index(): return HTML_PAGE + +@contextmanager +def get_db(db_path): + conn = sqlite3.connect(db_path) + conn.row_factory = sqlite3.Row + try: + yield conn + conn.commit() + finally: conn.close() + +def _db(request): return request.state.board["db"] + +def init_db(): + for board in BOARDS.values(): + with get_db(board["db"]) as db: + db.execute("""CREATE TABLE IF NOT EXISTS users ( + token TEXT PRIMARY KEY, name TEXT UNIQUE NOT NULL, created_at REAL)""") + db.execute("""CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL, + content TEXT NOT NULL, created_at REAL, + FOREIGN KEY(author) REFERENCES users(name))""") + db.execute("CREATE INDEX IF NOT EXISTS idx_posts_id ON posts(id)") + +def verify_token(token, db_path): + with get_db(db_path) as db: + row = db.execute("SELECT name FROM users WHERE token=?", (token,)).fetchone() + if not row: raise HTTPException(401, "invalid token") + return row["name"] + +@app.on_event("startup") +def startup(): + os.makedirs(UPLOAD_DIR, exist_ok=True) + load_boards_if_changed() + +@app.post("/register") +def register(request: Request, name=Body(..., embed=True)): + token = uuid.uuid4().hex[:16] + try: + with get_db(_db(request)) as db: + db.execute("INSERT INTO users VALUES(?,?,?)", (token, name, time.time())) + except sqlite3.IntegrityError: + with get_db(_db(request)) as db: + row = db.execute("SELECT token FROM users WHERE name=?", (name,)).fetchone() + return {"token": row["token"], "name": name} + return {"token": token, "name": name} + +@app.post("/post") +def create_post(request: Request, token=Body(...), content=Body(...)): + author = verify_token(token, _db(request)) + with get_db(_db(request)) as db: + cur = db.execute("INSERT INTO posts(author,content,created_at) VALUES(?,?,?)", + (author, content, time.time())) + post_id = cur.lastrowid + _T[0]=time.time() + return {"id": post_id, "author": author} + +@app.get("/poll") +def poll(request: Request, since_id=Query(0), limit=Query(50)): + with get_db(_db(request)) as db: + rows = db.execute("SELECT id,author,content,created_at FROM posts WHERE id>? ORDER BY id LIMIT ?", + (since_id, limit)).fetchall() + return [dict(r) for r in rows] + +@app.get("/count") +def count_posts(request: Request, author=Query(None)): + with get_db(_db(request)) as db: + q, p = ("SELECT COUNT(*) c FROM posts WHERE author=?", (author,)) if author else ("SELECT COUNT(*) c FROM posts", ()) + return {"total": db.execute(q, p).fetchone()["c"]} + +@app.get("/authors") +def get_authors(request: Request): + with get_db(_db(request)) as db: + return [r["author"] for r in db.execute("SELECT DISTINCT author FROM posts ORDER BY author").fetchall()] + +@app.get("/posts") +def get_posts(request: Request, author=Query(None), limit=Query(50), offset=Query(0)): + with get_db(_db(request)) as db: + if author: + rows = db.execute("SELECT id,author,content,created_at FROM posts WHERE author=? ORDER BY id DESC LIMIT ? OFFSET ?", + (author, limit, offset)).fetchall() + else: + rows = db.execute("SELECT id,author,content,created_at FROM posts ORDER BY id DESC LIMIT ? OFFSET ?", + (limit, offset)).fetchall() + return [dict(r) for r in rows] + +@app.post("/file/upload") +def upload_file(request: Request, token=Body(...), file: UploadFile = File(...)): + verify_token(token, _db(request)) + rand_id = uuid.uuid4().hex[:6] + safe_name = os.path.basename(file.filename) + dest = os.path.join(UPLOAD_DIR, rand_id) + os.makedirs(dest, exist_ok=True) + with open(os.path.join(dest, safe_name), "wb") as f: + f.write(file.file.read()) + return {"ref": f"{rand_id}/{safe_name}"} + +@app.get("/file/{rand_id}/{filename}") +def download_file(rand_id: str, filename: str): + path = os.path.join(UPLOAD_DIR, rand_id, os.path.basename(filename)) + if not os.path.exists(path): + raise HTTPException(404, "not found") + return FileResponse(path, filename=filename) + +if __name__ == "__main__": + import argparse, uvicorn + p = argparse.ArgumentParser(); p.add_argument("--cwd"); p.add_argument("--port", type=int, default=58800); p.add_argument("--key") + a = p.parse_args(); + if a.cwd: os.chdir(a.cwd) + if a.key: BOARDS_FILE = None; BOARDS.clear(); BOARDS[a.key] = {"name": "default", "db": f"{a.key}.db"}; Thread(target=lambda:[time.sleep(3600) or time.time()-_T[0]>172800 and os._exit(0) for _ in iter(int,1)],daemon=True).start() + uvicorn.run(app, host="0.0.0.0", port=a.port) \ No newline at end of file diff --git a/assets/code_run_header.py b/assets/code_run_header.py new file mode 100644 index 000000000..59fb3416b --- /dev/null +++ b/assets/code_run_header.py @@ -0,0 +1,27 @@ +import sys, os, json, re, time, subprocess +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'memory')) +_r = subprocess.run +def _d(b): + if not b: return '' + if isinstance(b, str): return b + try: return b.decode() + except: return b.decode('gbk', 'replace') +def _run(*a, **k): + t = k.pop('text', 0) | k.pop('universal_newlines', 0) + enc = k.pop('encoding', None) + k.pop('errors', None) + if enc: t = 1 + if t and isinstance(k.get('input'), str): + k['input'] = k['input'].encode() + r = _r(*a, **k) + if t: + if r.stdout is not None: r.stdout = _d(r.stdout) + if r.stderr is not None: r.stderr = _d(r.stderr) + return r +subprocess.run = _run +_Pi = subprocess.Popen.__init__ +def _pinit(self, *a, **k): + if os.name == 'nt': k['creationflags'] = (k.get('creationflags') or 0) | 0x08000000 + _Pi(self, *a, **k) +subprocess.Popen.__init__ = _pinit +sys.excepthook = lambda t, v, tb: (sys.__excepthook__(t, v, tb), print(f"\n[Agent Hint]: NO GUESSING! You MUST probe first. If missing common package, pip.")) if issubclass(t, (ImportError, AttributeError)) else sys.__excepthook__(t, v, tb) diff --git a/assets/configure_mykey.py b/assets/configure_mykey.py new file mode 100644 index 000000000..755fae8fd --- /dev/null +++ b/assets/configure_mykey.py @@ -0,0 +1,1430 @@ +#!/usr/bin/env python3 +""" +GenericAgent — 交互式初始化向导 (configure.py) +一键配置 LLM 模型 + 消息平台,自动生成 mykey.py + +用法: + python configure.py +""" + +import ast +import os +import sys +import re +import shutil +import json +import urllib.request +from datetime import datetime + +# ── ANSI 颜色 ────────────────────────────────────────────────────────────── +C = { + 'reset': '\033[0m', 'bold': '\033[1m', 'dim': '\033[2m', + 'red': '\033[91m', 'green': '\033[92m', 'yellow': '\033[93m', + 'blue': '\033[94m', 'magenta': '\033[95m', 'cyan': '\033[96m', 'white': '\033[97m', +} + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MYKPY_PATH = os.path.join(PROJECT_ROOT, 'mykey.py') + +# ── 模型厂商定义 ─────────────────────────────────────────────────────────── + +LLM_PROVIDERS = [ + # ═══════════════════════════ 通用协议(官方直连或任意兼容中转)═══════════════════════════ + { + 'id': 'oai_chat', + 'name': 'OpenAI Chat Completions 协议', + 'desc': '官方直连或任意 OAI 兼容中转/网关,自填 apibase(回车=OpenAI 官方)', + 'type': 'native_oai', + 'template': { + 'name': 'gpt-native', 'apikey': 'sk-', + 'apibase': 'https://api.openai.com/v1', 'model': 'gpt-5.5', + 'api_mode': 'chat_completions', 'reasoning_effort': 'high', + 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120, + }, + 'key_hint': '官方在 https://platform.openai.com/api-keys 获取;中转站填其提供的 Key', + 'model_choices': ['gpt-5.5', 'gpt-5.4'], + 'extra_fields': [ + {'key': 'apibase', 'label': 'API Base(官方或中转地址)', 'default': 'https://api.openai.com/v1'}, + ], + }, + { + 'id': 'oai_responses', + 'name': 'OpenAI Responses 协议', + 'desc': 'Responses API(o 系列/GPT-5.5 推荐端点),官方或兼容网关,自填 apibase', + 'type': 'native_oai', + 'template': { + 'name': 'gpt-responses', 'apikey': 'sk-', + 'apibase': 'https://api.openai.com/v1', 'model': 'gpt-5.5', + 'api_mode': 'responses', 'reasoning_effort': 'high', + 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120, + }, + 'key_hint': '官方在 https://platform.openai.com/api-keys 获取;中转站填其提供的 Key', + 'model_choices': ['gpt-5.5', 'gpt-5.4'], + 'extra_fields': [ + {'key': 'apibase', 'label': 'API Base(官方或中转地址)', 'default': 'https://api.openai.com/v1'}, + ], + }, + { + 'id': 'claude_messages', + 'name': 'Claude Messages 协议', + 'desc': 'Anthropic 官方直连或任意 Claude 兼容中转,自填 apibase(回车=官方)', + 'type': 'native_claude', + 'template': { + 'name': 'anthropic-direct', 'apikey': 'sk-ant-', + 'apibase': 'https://api.anthropic.com', 'model': 'claude-opus-4-7', + 'thinking_type': 'adaptive', 'max_tokens': 32768, 'temperature': 1, + }, + 'key_hint': '官方在 https://console.anthropic.com/ 获取;中转站填其提供的 Key', + 'model_choices': ['claude-opus-4-7', 'claude-sonnet-4-6'], + 'extra_fields': [ + {'key': 'apibase', 'label': 'API Base(官方或中转地址)', 'default': 'https://api.anthropic.com'}, + ], + }, + # ═══════════════════════════ 直连 API(按旗舰能力降序)═══════════════════════════ + { + 'id': 'deepseek', + 'name': 'DeepSeek (v4-Pro / Flash)', + 'desc': '开源模型,v4-Pro 旗舰 1M 上下文', + 'type': 'native_oai', + 'template': { + 'name': 'deepseek', 'apikey': 'sk-', + 'apibase': 'https://api.deepseek.com', 'model': 'deepseek-v4-pro', + 'api_mode': 'chat_completions', 'reasoning_effort': 'high', + }, + 'key_hint': '在 https://platform.deepseek.com/api_keys 获取', + 'model_choices': ['deepseek-v4-pro', 'deepseek-v4-flash'], + }, + { + 'id': 'kimi', + 'name': 'Kimi (k2.6 / k2.5) 双协议', + 'desc': '月之暗面,支持 Anthropic 和 OAI 双协议', + 'type': 'native_claude', + 'template': { + 'name': 'kimi', 'apikey': 'sk-kimi-', + 'apibase': 'https://api.kimi.com/coding', + 'model': 'kimi-for-coding', 'fake_cc_system_prompt': True, + 'thinking_type': 'adaptive', + }, + 'key_hint': '在 https://kimi.com/code 或 https://platform.moonshot.cn/ 获取', + 'model_choices': ['kimi-k2.6', 'kimi-k2.5'], + 'extra_fields': [ + { + 'key': '_protocol', 'label': '选择 API 协议', + 'type': 'choice', + 'options': [ + {'id': 'native_claude', 'name': 'Anthropic 兼容 (推荐)', 'desc': 'kimi-for-coding 端点,CC 兼容', 'apibase': 'https://api.kimi.com/coding', 'fake_cc_system_prompt': True, 'model': 'kimi-for-coding'}, + {'id': 'native_oai', 'name': 'OpenAI 协议', 'desc': 'Moonshot OAI 端点,kimi-k2 系列', 'apibase': 'https://api.moonshot.cn/v1', 'model': 'kimi-k2.6'}, + ], + }, + ], + }, + { + 'id': 'qwen', + 'name': '阿里通义千问 (Qwen3.5 / 百炼)', + 'desc': '阿里云百炼,Qwen3 系列百万级上下文', + 'type': 'native_oai', + 'template': { + 'name': 'qwen', 'apikey': 'sk-', + 'apibase': 'https://dashscope.aliyuncs.com/compatible-mode/v1', + 'model': 'qwen3.6-max-preview', + 'api_mode': 'chat_completions', + }, + 'key_hint': '在 https://bailian.console.aliyun.com/ 获取 API Key', + 'model_choices': ['qwen3.6-max-preview', 'qwen3.5-plus', 'qwen3-coder-plus'], + 'extra_fields': [ + { + 'key': '_endpoint', 'label': '选择端点', + 'type': 'choice', + 'options': [ + {'id': 'standard', 'name': '标准按量付费', 'desc': 'dashscope.aliyuncs.com,兼容模式', 'apibase': 'https://dashscope.aliyuncs.com/compatible-mode/v1'}, + {'id': 'coding_plan', 'name': '百炼 Coding Plan (订阅)', 'desc': 'coding-intl.dashscope.aliyuncs.com,100万上下文', 'apibase': 'https://coding-intl.dashscope.aliyuncs.com/v1', 'context_win': 1000000}, + ], + }, + ], + }, + { + 'id': 'zhipu', + 'name': '智谱 GLM-5.1 (Coding Plan)', + 'desc': '智谱 GLM,支持 Coding Plan CN (Anthropic) 和 Global (OAI) 双端点', + 'type': 'native_claude', + 'template': { + 'name': 'zhipu-glm', 'apikey': 'sk-', + 'apibase': 'https://open.bigmodel.cn/api/anthropic', + 'model': 'GLM-5.1-Cloud', 'fake_cc_system_prompt': False, + 'thinking_type': 'adaptive', 'max_retries': 3, + 'connect_timeout': 10, 'read_timeout': 180, + }, + 'key_hint': 'CN 在 https://open.bigmodel.cn/ 获取;Global 在 https://z.ai/ 获取', + 'model_choices': ['GLM-5.1-Cloud', 'glm-4.7'], + 'extra_fields': [ + { + 'key': '_plan', 'label': '选择 Coding Plan', + 'type': 'choice', + 'options': [ + {'id': 'native_claude', 'name': 'Coding Plan CN (Anthropic)', 'desc': 'open.bigmodel.cn,推荐国内用户', 'apibase': 'https://open.bigmodel.cn/api/anthropic', 'fake_cc_system_prompt': False}, + {'id': 'native_oai', 'name': 'Coding Plan Global (OAI)', 'desc': 'api.z.ai,OpenAI 协议,全球可用', 'apibase': 'https://api.z.ai/api/paas/v4'}, + ], + }, + ], + }, + { + 'id': 'minimax', + 'name': 'MiniMax M3 (双协议)', + 'desc': 'MiniMax M3,支持 Anthropic 和 OpenAI 双协议', + 'type': 'native_claude', + 'template': { + 'name': 'minimax', 'apikey': 'eyJh...', + 'apibase': 'https://api.minimaxi.com/anthropic', + 'model': 'MiniMax-M3', 'max_retries': 3, + }, + 'key_hint': '在 https://platform.minimaxi.com/user-center/basic-information 获取', + 'model_choices': ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'], + 'extra_fields': [ + { + 'key': '_protocol', 'label': '选择 API 协议', + 'type': 'choice', + 'options': [ + {'id': 'native_claude', 'name': 'Anthropic 协议 (推荐)', 'desc': '无 标签,原生 Claude 兼容', 'apibase': 'https://api.minimaxi.com/anthropic'}, + {'id': 'native_oai', 'name': 'OpenAI 协议', 'desc': '走 /v1/chat/completions', 'apibase': 'https://api.minimaxi.com/v1', 'context_win': 50000}, + ], + }, + ], + }, + { + 'id': 'stepfun', + 'name': '阶跃星辰 Step-3.5 (推理强)', + 'desc': '阶跃星辰 Step 系列,支持标准和 Step Plan 双端点', + 'type': 'native_oai', + 'template': { + 'name': 'stepfun', 'apikey': 'sk-', + 'apibase': 'https://api.stepfun.com/v1', + 'model': 'step-3.5-flash', + 'api_mode': 'chat_completions', + 'context_win': 262144, + }, + 'key_hint': '在 https://platform.stepfun.com/ 获取 API Key', + 'model_choices': ['step-3.5-flash', 'step-3.5-flash-2603'], + 'extra_fields': [ + { + 'key': '_endpoint', 'label': '选择端点', + 'type': 'choice', + 'options': [ + {'id': 'standard', 'name': '标准端点', 'desc': 'api.stepfun.com/v1,按量付费', 'apibase': 'https://api.stepfun.com/v1', 'context_win': 262144}, + {'id': 'step_plan', 'name': 'Step Plan (订阅)', 'desc': 'api.stepfun.com/step_plan/v1,订阅制', 'apibase': 'https://api.stepfun.com/step_plan/v1', 'context_win': 262144}, + ], + }, + ], + }, + { + 'id': 'qianfan', + 'name': '百度千帆 (ERNIE 5.0 / 第三方)', + 'desc': '百度智能云千帆,文心一言 ERNIE 5.0 + DeepSeek 等', + 'type': 'native_oai', + 'template': { + 'name': 'baidu-qianfan', 'apikey': '', + 'apibase': 'https://qianfan.baidubce.com/v2', + 'model': 'ernie-5.0-thinking-preview', + 'api_mode': 'chat_completions', + }, + 'key_hint': '在 https://console.bce.baidu.com/qianfan/ 创建应用获取 API Key', + 'model_choices': ['ernie-5.0-thinking-preview', 'deepseek-v3.2'], + 'extra_fields': [ + {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://qianfan.baidubce.com/v2'}, + ], + }, + { + 'id': 'volcengine', + 'name': '火山引擎 (豆包 / Ark)', + 'desc': '字节跳动火山引擎,支持标准 Ark 和 Ark Coding Plan', + 'type': 'native_oai', + 'template': { + 'name': 'volc-ark', 'apikey': '', + 'apibase': 'https://ark.cn-beijing.volces.com/api/v3', + 'model': 'doubao-seed-code-preview-251028', + 'api_mode': 'chat_completions', + }, + 'key_hint': '在 https://console.volcengine.com/ark/ 创建推理接入点后获取 API Key', + 'model_choices': ['doubao-seed-code-preview-251028', 'doubao-seed-1-8-251228'], + 'extra_fields': [ + { + 'key': '_endpoint', 'label': '选择端点', + 'type': 'choice', + 'options': [ + {'id': 'standard', 'name': '标准 Ark', 'desc': 'ark.cn-beijing.volces.com/api/v3,按量付费', 'apibase': 'https://ark.cn-beijing.volces.com/api/v3'}, + {'id': 'coding_plan', 'name': 'Ark Coding Plan (订阅)', 'desc': 'ark.cn-beijing.volces.com/api/coding/v3', 'apibase': 'https://ark.cn-beijing.volces.com/api/coding/v3'}, + ], + }, + ], + }, + { + 'id': 'xiaomi', + 'name': '小米 MiMo (MiMo 2.5 Pro / TokenPlan)', + 'desc': '小米 MiMo 系列,超大上下文窗口,支持 TokenPlan 预付费', + 'type': 'native_oai', + 'template': { + 'name': 'xiaomi-mimo', 'apikey': 'sk-', + 'apibase': 'https://api.xiaomimimo.com/v1', + 'model': 'mimo-v2.5-pro', + 'api_mode': 'chat_completions', + }, + 'key_hint': '在 https://x.xiaomi.com/ 获取 API Key', + 'model_choices': ['mimo-v2.5-pro', 'mimo-v2-flash'], + 'extra_fields': [ + {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://api.xiaomimimo.com/v1'}, + ], + }, + { + 'id': 'tencent_tokenhub', + 'name': '腾讯混元 TokenHub (Hy3 / TokenPlan)', + 'desc': '腾讯云 TokenHub,混元 Hy3 系列,TokenPlan 预付费', + 'type': 'native_oai', + 'template': { + 'name': 'tencent-tokenhub', 'apikey': 'sk-', + 'apibase': 'https://tokenhub.tencentmaas.com/v1', + 'model': 'hy3-preview', + 'api_mode': 'chat_completions', + }, + 'key_hint': '在 https://console.cloud.tencent.com/tokenhub 获取 API Key', + 'model_choices': ['hy3-preview'], + 'extra_fields': [ + {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://tokenhub.tencentmaas.com/v1'}, + ], + }, + # ═══════════════════════════ 代理 / 中继(支持 Claude/GPT 等顶级模型)══════════ + { + 'id': 'cc_relay', + 'name': 'CC Switch 透传 (社区常用)', + 'desc': '社区 Claude Code 透传渠道,可接入 Claude Opus', + 'type': 'native_claude', + 'template': { + 'name': 'cc-relay', 'apikey': 'sk-user-', + 'apibase': 'https:///claude/office', + 'model': 'claude-opus-4-7', 'fake_cc_system_prompt': True, + 'thinking_type': 'adaptive', + }, + 'key_hint': '从你的 CC Switch 服务商获取 apikey 和 apibase', + 'model_choices': ['claude-opus-4-7', 'claude-sonnet-4-6'], + 'extra_fields': [ + {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://your-host/claude/office'}, + {'key': 'fake_cc_system_prompt', 'label': 'fake_cc_system_prompt', 'type': 'bool', 'default': True}, + ], + }, + { + 'id': 'openrouter', + 'name': 'OpenRouter (多模型中继)', + 'desc': '一个 Key 通吃 Claude/GPT/DeepSeek/Qwen 等', + 'type': 'native_oai', + 'template': { + 'name': 'openrouter', 'apikey': 'sk-or-', + 'apibase': 'https://openrouter.ai/api/v1', + 'model': 'anthropic/claude-opus-4-7', + 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120, + }, + 'key_hint': '在 https://openrouter.ai/keys 获取', + 'model_choices': ['anthropic/claude-opus-4-7', 'openai/gpt-5.5'], + }, + { + 'id': 'commonstack', + 'name': 'CommonStack (统一网关)', + 'desc': '一个 Key 通吃 Claude/GPT/Gemini/DeepSeek/MiniMax/Zhipu/xAI 等', + 'type': 'native_oai', + 'template': { + 'name': 'commonstack', 'apikey': 'sk-', + 'apibase': 'https://api.commonstack.ai/v1', + 'model': 'anthropic/claude-opus-4-7', + 'api_mode': 'chat_completions', + 'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120, + }, + 'key_hint': '在 https://commonstack.ai 注册后从 Dashboard 获取 API Key', + 'model_choices': ['anthropic/claude-opus-4-7', 'openai/gpt-5.5'], + }, + { + 'id': 'crs', + 'name': 'CRS 反代 (Claude Max 多通道)', + 'desc': 'CRS 协议的反代服务,支持 Claude Max / Gemini Ultra 通道', + 'type': 'native_claude', + 'template': { + 'name': 'crs', 'apikey': 'cr_', + 'apibase': 'https:///api', + 'model': 'claude-opus-4-7[1m]', 'fake_cc_system_prompt': True, + 'thinking_type': 'adaptive', 'max_tokens': 32768, + 'max_retries': 3, 'read_timeout': 180, + }, + 'key_hint': '从你的 CRS 服务商获取 key 和 host', + 'model_choices': ['claude-opus-4-7[1m]', 'claude-sonnet-4-6'], + 'extra_fields': [ + { + 'key': '_channel', 'label': '选择 CRS 通道', + 'type': 'choice', + 'options': [ + {'id': 'claude_max', 'name': 'Claude Max (默认)', 'desc': '标准 CRS Claude 通道', 'apibase': 'https:///api'}, + {'id': 'gemini_ultra', 'name': 'Gemini Ultra (Antigravity)', 'desc': 'CRS 包装的 Google Antigravity,不支持 SSE 流式', 'apibase': 'https:///antigravity/api', 'model': 'claude-opus-4-7-thinking', 'stream': False}, + ], + }, + ], + }, + { + 'id': 'gmi', + 'name': 'GMI Serving (通用模型中继)', + 'desc': 'GMI 通用模型推理服务,支持多种开源/闭源(手动输入模型名)', + 'type': 'native_oai', + 'template': { + 'name': 'gmi', 'apikey': '', + 'apibase': 'https://api.gmi-serving.com/v1', + 'model': 'gmi-default', + 'api_mode': 'chat_completions', + }, + 'key_hint': '从 GMI 服务商获取 API Key,探测失败时手动输入模型名', + 'model_choices': [], # 中继服务,模型由服务商提供,探测失败时手动输入 + 'extra_fields': [ + {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://api.gmi-serving.com/v1'}, + ], + }, +] + +# ── 消息平台定义 ──────────────────────────────────────────────────────────── +PLATFORMS = [ + { + 'id': 'none', + 'name': '不使用消息平台(纯终端 REPL)', + 'desc': '直接用 python agentmain.py 在终端交互', + 'deps': [], + }, + { + 'id': 'telegram', + 'name': 'Telegram 机器人', + 'desc': '通过 Telegram Bot 与 Agent 对话', + 'file': 'frontends/tgapp.py', + 'deps': ['python-telegram-bot'], + 'env_vars': [ + {'key': 'tg_bot_token', 'label': 'Bot Token', 'hint': '从 @BotFather 获取'}, + {'key': 'tg_allowed_users', 'label': '允许的用户 ID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True}, + ], + }, + { + 'id': 'qq', + 'name': 'QQ 机器人', + 'desc': '通过 QQ 官方机器人 API 接入', + 'file': 'frontends/qqapp.py', + 'deps': ['qq-botpy'], + 'env_vars': [ + {'key': 'qq_app_id', 'label': 'App ID', 'hint': 'QQ 开放平台获取'}, + {'key': 'qq_app_secret', 'label': 'App Secret'}, + {'key': 'qq_allowed_users', 'label': '允许的用户 OpenID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True}, + ], + }, + { + 'id': 'feishu', + 'name': '飞书机器人', + 'desc': '通过飞书应用与 Agent 对话', + 'file': 'frontends/fsapp.py', + 'deps': ['lark-oapi'], + 'env_vars': [ + {'key': 'fs_app_id', 'label': 'App ID', 'hint': '飞书开放平台获取'}, + {'key': 'fs_app_secret', 'label': 'App Secret'}, + {'key': 'fs_allowed_users', 'label': '允许的用户(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True}, + ], + }, + { + 'id': 'wecom', + 'name': '企业微信机器人', + 'desc': '通过企业微信 Bot 接入', + 'file': 'frontends/wecomapp.py', + 'deps': ['wecombot'], + 'env_vars': [ + {'key': 'wecom_bot_id', 'label': 'Bot ID'}, + {'key': 'wecom_secret', 'label': 'Bot Secret'}, + {'key': 'wecom_allowed_users', 'label': '允许的用户(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True}, + ], + }, + { + 'id': 'dingtalk', + 'name': '钉钉机器人', + 'desc': '通过钉钉应用接入', + 'file': 'frontends/dingtalkapp.py', + 'deps': ['dingtalk-sdk'], + 'env_vars': [ + {'key': 'dingtalk_client_id', 'label': 'Client ID (App Key)'}, + {'key': 'dingtalk_client_secret', 'label': 'Client Secret (App Secret)'}, + {'key': 'dingtalk_allowed_users', 'label': '允许的用户 StaffID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True}, + ], + }, + { + 'id': 'discord', + 'name': 'Discord 机器人', + 'desc': '通过 Discord Bot 接入', + 'file': 'frontends/dcapp.py', + 'deps': ['discord.py'], + 'env_vars': [ + {'key': 'dc_bot_token', 'label': 'Bot Token', 'hint': 'Discord Developer Portal 获取'}, + {'key': 'dc_allowed_users', 'label': '允许的用户 ID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True}, + ], + }, + { + 'id': 'wechat', + 'name': '微信 (iLink 协议)', + 'desc': '通过微信个人号与 Agent 对话,扫码自动登录', + 'file': 'frontends/wechatapp.py', + 'deps': ['requests', 'qrcode', 'pycryptodome'], + 'env_vars': [], + }, +] + + +def _masked(v, reveal, tail): + """生成脱敏字符串:前 reveal 位明文 + * + 后 tail 位明文""" + if len(v) > reveal + tail: + return v[:reveal] + '*' * min(len(v) - reveal - tail, 8) + v[-tail:] + elif len(v) > reveal: + return v[:reveal] + '*' * (len(v) - reveal) + return v + +def masked_input(prompt, reveal=6, tail=4): + """密文输入,支持粘贴:批读取 + 延迟重绘,避免快速键入时丢字符。 + + prompt 必须为单行(不含 \\n)。 + """ + sys.stdout.write(prompt) + sys.stdout.flush() + chars = [] + + def _repaint(): + m = _masked(''.join(chars), reveal, tail) + sys.stdout.write(f'\r{prompt}{m} \r{prompt}{m}') + sys.stdout.flush() + + def _process(c): + """处理单个字符,返回 True 表示应退出。""" + if c in ('\r', '\n'): + return True + if c in ('\x03', '\x04'): + raise KeyboardInterrupt + if c in ('\x08', '\x7f'): + if chars: + chars.pop() + elif c.isprintable() or c == ' ': + chars.append(c) + return False + + if os.name == 'nt': + import msvcrt + while True: + c = msvcrt.getwch() + if _process(c): + break + if c in ('\x08', '\x7f'): + _repaint() # 退格立即重绘 + continue + if not (c.isprintable() or c == ' '): + continue + # 批量读取:粘贴时一次取完 + while msvcrt.kbhit(): + c2 = msvcrt.getwch() + if _process(c2): + value = ''.join(chars) + _repaint() + sys.stdout.write('\n') + sys.stdout.flush() + return value + _repaint() + else: + import tty, termios, select + fd = sys.stdin.fileno() + old = termios.tcgetattr(fd) + try: + tty.setraw(fd) + while True: + c = sys.stdin.read(1) + if _process(c): + break + if c in ('\x08', '\x7f'): + _repaint() # 退格立即重绘 + continue + if not (c.isprintable() or c == ' '): + continue + # 批量读取:只要 stdin 有数据就继续读,不重绘 + while select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []): + c2 = sys.stdin.read(1) + if _process(c2): + value = ''.join(chars) + _repaint() + termios.tcsetattr(fd, termios.TCSADRAIN, old) + sys.stdout.write('\n') + sys.stdout.flush() + return value + _repaint() + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + + value = ''.join(chars) + _repaint() + sys.stdout.write('\n') + sys.stdout.flush() + return value + + +# ═══════════════════════════════════════════════════════════════════════════ +# UI Helpers +# ═══════════════════════════════════════════════════════════════════════════ + +def cprint(text, color=None, bold=False, end='\n'): + parts = [] + if color: parts.append(C.get(color, '')) + if bold: parts.append(C['bold']) + parts.append(text) + parts.append(C['reset']) + print(''.join(parts), end=end) + +def banner(): + print('\033[2J\033[H', end='') # ANSI 清屏,跨平台 + print(f"{C['cyan']}{C['bold']}") + print(" ╔═══════════════════════════════════════════════════════════╗") + print(" ║ GenericAgent — 交互式初始化向导 v1.2 ║") + print(" ║ 一键配置 LLM 模型 + 消息平台,自动生成 mykey.py ║") + print(" ╚═══════════════════════════════════════════════════════════╝") + print(f"{C['reset']}") + print(f"{C['dim']} 项目目录: {PROJECT_ROOT}{C['reset']}") + print() + +def _check_python(): + """检查 Python 版本,返回 (ok, msg)""" + vi = sys.version_info + if vi < (3, 10): + return False, f"Python {vi.major}.{vi.minor} 不满足最低要求 (≥ 3.10)" + if vi[:2] == (3, 12): + return True, '' + return True, f"⚠ 当前 Python {vi.major}.{vi.minor},推荐使用 Python 3.12" + +def ask_choice(prompt, choices, allow_multi=False, default=None): + """交互式选择,返回 selected_id 或 [selected_ids]""" + print(f"\n{C['bold']}{prompt}{C['reset']}") + if allow_multi: + print(f"{C['dim']} (可多选,输入序号用逗号分隔,如: 1,3,5;输入 a 全选;回车跳过){C['reset']}") + else: + print(f"{C['dim']} (输入序号,如: 1){C['reset']}") + for i, c in enumerate(choices, 1): + desc = c.get('desc', '') + print(f" {C['green']}{i}.{C['reset']} {C['bold']}{c['name']}{C['reset']} {C['dim']}{desc}{C['reset']}") + while True: + raw = input(f"\n {C['yellow']}►{C['reset']} ").strip() + if not raw and default is not None: + return default + if allow_multi: + if raw.lower() == 'a': + return [c['id'] for c in choices] + parts = [p.strip() for p in raw.split(',') if p.strip()] + selected = [] + for p in parts: + try: + idx = int(p) - 1 + if 0 <= idx < len(choices): + selected.append(choices[idx]['id']) + except ValueError: + pass + if selected: + return selected + else: + try: + idx = int(raw) - 1 + if 0 <= idx < len(choices): + return choices[idx]['id'] + except ValueError: + pass + print(f" {C['red']}✗ 请输入有效序号{C['reset']}") + +def ask_input(prompt, default=None, secret=False, hint=None): + """交互式输入。secret=True 时使用脱敏输入。""" + if hint: + cprint(f" {hint}", 'dim') + if default is not None: + cprint(f" [默认: {default}]", 'dim') + prompt_line = f" {C['yellow']}►{C['reset']} {prompt}: " + while True: + if secret: + val = masked_input(prompt_line).strip() + else: + val = input(prompt_line).strip() + if not val and default is not None: + return default + if val: + return val + cprint("✗ 此项不能为空", 'red') + +def ask_yesno(prompt, default=True): + hint = "Y/N" + raw = input(f"\n {C['yellow']}►{C['reset']} {prompt} ({hint}): ").strip().lower() + if not raw: + return default + return raw.startswith('y') + + +# ═══════════════════════════════════════════════════════════════════════════ +# LLM 配置逻辑 +# ═══════════════════════════════════════════════════════════════════════════ + +def _get_proxy_handler(): + """从环境变量读取代理配置,返回 ProxyHandler 或 None""" + for var in ('HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy'): + url = os.environ.get(var) + if url: + return urllib.request.ProxyHandler({'https': url, 'http': url}) + return None + +def probe_models(provider, apikey, apibase=None): + """调用 API 探测可用模型列表,返回模型 ID 列表或 None""" + ptype = provider.get('type', 'native_oai') + base = (apibase or provider['template'].get('apibase', '')).rstrip('/') + + if ptype == 'native_claude': + url = f"{base}/v1/models" + headers = {'x-api-key': apikey, 'anthropic-version': '2023-06-01', 'User-Agent': 'GenericAgent/1.0'} + else: + url = f"{base}/models" + headers = {'Authorization': f'Bearer {apikey}', 'User-Agent': 'GenericAgent/1.0'} + + print(f"\n {C['dim']}🔍 正在探测可用模型 ({base}/models)...{C['reset']}", end='', flush=True) + if ptype == 'native_claude': + print(f" {C['dim']}(Anthropic 协议,探测可能失败){C['reset']}", end='', flush=True) + + opener = urllib.request.build_opener() + ph = _get_proxy_handler() + if ph: + opener = urllib.request.build_opener(ph) + print(f" {C['dim']}(via proxy){C['reset']}", end='', flush=True) + + for attempt in range(2): + try: + req = urllib.request.Request(url, headers=headers, method='GET') + with opener.open(req, timeout=10) as resp: + data = json.loads(resp.read().decode()) + models = data.get('data', []) + ids = sorted(set(m['id'] for m in models if isinstance(m, dict) and m.get('id'))) + if ids: + print(f" {C['green']}✓ 发现 {len(ids)} 个模型{C['reset']}") + return ids + print(f" {C['yellow']}⚠ 返回为空{C['reset']}") + return None + except Exception as e: + if attempt == 0 and 'timeout' in type(e).__name__.lower(): + print(f" {C['yellow']}⏱ 超时,重试...{C['reset']}", end='', flush=True) + continue + print(f" {C['yellow']}⚠ 探测失败: {type(e).__name__}(将使用预设列表){C['reset']}") + return None + return None + +def _normalize_model_choices(choices): + """统一 model_choices 格式为 [{'id': str, 'name': str}]""" + if not choices: + return [] + result = [] + for item in choices: + if isinstance(item, str): + result.append({'id': item, 'name': item}) + elif isinstance(item, dict): + result.append(item) + elif isinstance(item, (tuple, list)) and len(item) >= 1: + result.append({'id': item[0], 'name': item[1] if len(item) > 1 else item[0]}) + return result + +def _configure_advanced(provider, cfg): + """配置高级可选字段: proxy, context_win, stream, user_agent, thinking_budget_tokens""" + print(f"\n {C['dim']}── 高级选项(回车跳过,使用默认值){C['reset']}") + proxy = ask_input("HTTP 代理地址 (proxy)", default='', hint='如 http://127.0.0.1:2082,留空跳过') + if proxy: + cfg['proxy'] = proxy + cw = ask_input("上下文窗口阈值 (context_win)", default='', hint='默认 30000,DeepSeek 默认 70000;联动压缩与工具上限') + if cw: + cfg['context_win'] = int(cw) + if cfg.get('thinking_type') == 'enabled': + tbt = ask_input("thinking_budget_tokens", default='', hint='low≈4096, medium≈10240, high≈32768') + if tbt: + cfg['thinking_budget_tokens'] = int(tbt) + if cfg.get('type', provider['type']) == 'native_claude': + ua = ask_input("User-Agent 版本号", default='', hint='某些中转按 UA 白名单校验,pin 老版本用') + if ua: + cfg['user_agent'] = ua + stream_default = cfg.get('stream', True) + if ask_yesno("启用 SSE 流式 (stream)", default=stream_default): + cfg['stream'] = True + else: + cfg['stream'] = False + +def configure_llm(provider): + """引导用户配置单个模型""" + print(f"\n{C['cyan']}{'─'*60}{C['reset']}") + print(f"{C['bold']} 配置: {provider['name']}{C['reset']}") + print(f" {C['dim']}{provider['desc']}{C['reset']}") + print(f"{C['cyan']}{'─'*60}{C['reset']}") + + cfg = dict(provider['template']) + + # API Key(密文输入) + cfg['apikey'] = ask_input( + f"API Key", + hint=provider.get('key_hint', ''), + secret=True, + ) + + # 额外字段 + for field in provider.get('extra_fields', []): + if field['key'] == 'apibase': + cfg['apibase'] = ask_input( + field['label'], + default=field.get('default', cfg.get('apibase', '')), + ) + elif field.get('type') == 'bool': + cfg[field['key']] = ask_yesno( + field['label'], + default=field.get('default', True) + ) + elif field.get('type') == 'choice': + picked = ask_choice(field['label'], field['options']) + chosen = next(o for o in field['options'] if o['id'] == picked) + for opt_key, opt_val in chosen.items(): + if opt_key not in ('id', 'name', 'desc'): + cfg[opt_key] = opt_val + + # 模型选择 + manual_choice = {'id': '__manual__', 'name': '✏️ 手动输入模型名', 'desc': '自定义模型 ID,不依赖探测结果'} + model_list = probe_models(provider, cfg['apikey'], cfg.get('apibase')) + if model_list: + refresh_choice = {'id': '__refresh__', 'name': '🔄 重新探测'} + choices = [refresh_choice, manual_choice] + [{'id': m, 'name': m} for m in model_list] + while True: + picked = ask_choice("API 探测到以下可用模型(或手动输入):", choices) + if picked == '__refresh__': + print(f" {C['dim']}再次探测...{C['reset']}") + model_list = probe_models(provider, cfg['apikey'], cfg.get('apibase')) + if not model_list: + print(f" {C['yellow']}⚠ 再次探测失败{C['reset']}") + picked = _fallback_model(provider, manual_choice) + break + choices = [refresh_choice, manual_choice] + [{'id': m, 'name': m} for m in model_list] + elif picked == '__manual__': + picked = ask_input("请输入模型名", default=cfg.get('model', '')) + break + else: + break + cfg['model'] = picked + else: + cfg['model'] = _fallback_model(provider, manual_choice) + + # 别名 + default_name = cfg.get('name', provider['id']) + name = ask_input("此配置的别名 (name,Mixin 引用用)", default=default_name) + if name: + cfg['name'] = name + + # 高级选项 + if ask_yesno("配置高级选项(proxy / context_win / stream 等)?", default=False): + _configure_advanced(provider, cfg) + + return cfg + +def _fallback_model(provider, manual_choice=None): + """使用预设模型列表让用户选择,始终提供手动输入选项""" + manual_choice = manual_choice or {'id': '__manual__', 'name': '✏️ 手动输入模型名', 'desc': '自定义模型 ID'} + normalized = _normalize_model_choices(provider.get('model_choices', [])) + if normalized: + choices = [manual_choice] + normalized + picked = ask_choice("选择模型(或手动输入):", choices) + if picked == '__manual__': + return ask_input("请输入模型名", default=provider['template'].get('model', '')) + return picked + return ask_input("请输入模型名", default=provider['template'].get('model', '')) + +def configure_llms(): + """配置 LLM 模型""" + print(f"\n{C['bold']}{C['magenta']}╔══════════════════════════════════════╗") + print(f"║ 第一步: 配置 LLM 模型 ║") + print(f"╚══════════════════════════════════════╝{C['reset']}") + print(f"\n{C['dim']} 你可以配置最多 2 个模型组成故障转移 (Mixin) 列表。{C['reset']}") + + all_cfgs = [] + provider_id = ask_choice("选择模型厂商 (配置第 1 个模型):", LLM_PROVIDERS) + provider = next(p for p in LLM_PROVIDERS if p['id'] == provider_id) + cfg = configure_llm(provider) + all_cfgs.append(cfg) + + if ask_yesno("再添加一个模型做故障转移?", default=False): + providers_ext = [{'id': '__stop__', 'name': '✓ 不需要备选了', 'desc': ''}] + LLM_PROVIDERS + provider_id = ask_choice( + "选择模型厂商 (配置第 2 个模型 — 或选「不需要备选了」跳过):", + providers_ext + ) + if provider_id != '__stop__': + provider = next(p for p in LLM_PROVIDERS if p['id'] == provider_id) + cfg = configure_llm(provider) + all_cfgs.append(cfg) + + return all_cfgs + + +# ═══════════════════════════════════════════════════════════════════════════ +# 消息平台配置逻辑 +# ═══════════════════════════════════════════════════════════════════════════ + +def configure_platforms(): + """配置消息平台,返回 (platform_configs, pip_hints)""" + print(f"\n{C['bold']}{C['magenta']}╔══════════════════════════════════════╗") + print(f"║ 第二步: 配置消息平台 ║") + print(f"╚══════════════════════════════════════╝{C['reset']}") + print(f"\n{C['dim']} 消息平台用于从聊天软件与 Agent 交互。{C['reset']}") + print(f"{C['dim']} 你也可以跳过此步,直接用终端 REPL。{C['reset']}") + + platform_ids = ask_choice( + "选择消息平台 (可多选,选 '不使用' 则跳过):", + PLATFORMS, + allow_multi=True, + default=['none'] + ) + + if 'none' in platform_ids: + return [], set() + + selected_platforms = [] + pip_hints = set() + + for pid in platform_ids: + platform = next(p for p in PLATFORMS if p['id'] == pid) + pip_hints.update(platform.get('deps', [])) + + print(f"\n{C['cyan']}{'─'*60}{C['reset']}") + print(f"{C['bold']} 配置: {platform['name']}{C['reset']}") + print(f"{C['cyan']}{'─'*60}{C['reset']}") + + env_vals = {} + + if pid == 'feishu' and ask_yesno("使用一键扫码创建应用?(推荐)", default=True): + env_vals = _feishu_scan(platform) + if pid == 'wechat' and ask_yesno("扫码登录微信 iLink?(推荐)", default=True): + env_vals = _wechat_scan() + + for var in platform['env_vars']: + if var['key'] not in env_vals: + env_vals.update(_manual_platform_var(var)) + + if pid == 'wecom' and ask_yesno("设置欢迎消息?", default=False): + env_vals['wecom_welcome_message'] = ask_input("欢迎消息内容", default='你好,我在线上。') + + selected_platforms.append({'platform': platform, 'config': env_vals}) + + return selected_platforms, pip_hints + +def _manual_platform_var(var): + """手动填写单个平台变量""" + val = ask_input(var['label'], hint=var.get('hint', ''), default=var.get('default')) + if var.get('is_list'): + if val == '[]' or not val: + return {var['key']: []} + return {var['key']: [x.strip() for x in val.split(',') if x.strip()]} + return {var['key']: val} + +def _feishu_scan(platform): + """飞书一键扫码创建应用,返回 env_vals 或空 dict""" + from io import StringIO + try: + import lark_oapi as lark + import qrcode, threading + except ImportError: + print(f"\n {C['yellow']}⚠ lark-oapi 未安装,降级为手动配置{C['reset']}") + return {} + + print(f"\n {C['cyan']}📱 正在启动一键创建...{C['reset']}") + print(f" {C['dim']} 请用飞书 App 扫描终端二维码,完成授权后自动获取凭据。{C['reset']}\n") + + qr_printed = threading.Event() + result_holder = {'data': None} + + def handle_qr(info): + url = info['url'] + expire = info['expire_in'] + qr = qrcode.QRCode(border=1, box_size=1) + qr.add_data(url) + buf = StringIO() + qr.print_ascii(out=buf) + qr_art = buf.getvalue() + print(f"\n {C['bold']}请用飞书扫描下方二维码,或复制链接在浏览器打开:{C['reset']}") + print(f" {C['green']}{qr_art.replace(chr(27), '')}{C['reset']}") + print(f" {C['dim']} 链接: {url}{C['reset']}") + print(f" {C['dim']} 有效期 {expire} 秒{C['reset']}") + qr_printed.set() + + def handle_status(info): + status = info['status'] + if status == 'polling': + print(f" {C['yellow']}⏳ 等待扫码...{C['reset']}") + elif status == 'slow_down': + print(f" {C['yellow']}⏳ 等待中... (间隔 {info.get('interval', '?')}s){C['reset']}") + elif status == 'domain_switched': + print(f" {C['cyan']}🌐 已切换认证域名{C['reset']}") + + def run_register(): + try: + result = lark.register_app( + on_qr_code=handle_qr, + on_status_change=handle_status, + ) + result_holder['data'] = result + except Exception as e: + print(f"\n {C['red']}✗ 创建失败: {e}{C['reset']}") + + thread = threading.Thread(target=run_register, daemon=True) + thread.start() + qr_printed.wait(timeout=15) + thread.join(timeout=300) + + if result_holder['data']: + result = result_holder['data'] + print(f"\n {C['green']}✅ 应用创建成功!{C['reset']}") + print(f" App ID: {C['bold']}{result['client_id']}{C['reset']}") + print(f" App Secret: {C['bold']}{result['client_secret']}{C['reset']}") + return { + 'fs_app_id': result['client_id'], + 'fs_app_secret': result['client_secret'], + } + else: + print(f"\n {C['yellow']}⚠ 扫码创建未完成,降级为手动填写...{C['reset']}") + return {} + + +def _wechat_scan(): + """微信 iLink 扫码登录,保存 token 到 ~/.wxbot/token.json,返回 env_vals""" + print(f"\n {C['cyan']}📱 正在启动微信 iLink 扫码登录...{C['reset']}") + print(f" {C['dim']} 请用微信扫描终端二维码,完成授权后自动获取凭据。{C['reset']}\n") + + # 确保项目根在路径中,以便导入 frontends/wechatapp + if PROJECT_ROOT not in sys.path: + sys.path.insert(0, PROJECT_ROOT) + try: + from frontends.wechatapp import WxBotClient + except ImportError as e: + print(f"\n {C['yellow']}⚠ 无法导入 WxBotClient: {e}{C['reset']}") + return {} + + try: + bot = WxBotClient() + if bot.token: + print(f" {C['green']}✅ 已有有效 token (bot_id={bot.bot_id}){C['reset']}") + if ask_yesno("重新扫码登录?", default=False): + bot.token = '' + else: + return {} + bot.login_qr() + print(f"\n {C['green']}✅ 微信 iLink 扫码登录成功!{C['reset']}") + print(f" Bot ID: {C['bold']}{bot.bot_id}{C['reset']}") + print(f" Token 已保存到: {C['dim']}{bot._tf}{C['reset']}") + except Exception as e: + print(f"\n {C['red']}✗ 扫码登录失败: {e}{C['reset']}") + return {} + + return {} + + + +# ═══════════════════════════════════════════════════════════════════════════ +# 生成 mykey.py +# ═══════════════════════════════════════════════════════════════════════════ + +def _var_type_info(cfg): + """根据配置类型返回 (var_prefix, session_type)""" + cfg_type = cfg.get('type', 'native_oai') + if cfg_type == 'native_claude': + return 'native_claude_config', 'NativeClaudeSession' + elif cfg_type == 'claude': + return 'claude_config', 'ClaudeSession' + elif cfg_type == 'oai': + return 'oai_config', 'LLMSession' + else: + return 'native_oai_config', 'NativeOAISession' + + +def generate_mykey(llm_cfgs, platform_configs): + """生成 mykey.py 内容""" + lines = [] + lines.append("# ══════════════════════════════════════════════════════════════════════════════") + lines.append(f"# GenericAgent — mykey.py (由 configure.py 自动生成 @ {datetime.now().strftime('%Y-%m-%d %H:%M')})") + lines.append("# ══════════════════════════════════════════════════════════════════════════════") + lines.append("") + lines.append("# ── 停止符 ──────────────────────────────────────────────────────────────────") + lines.append("_SETUP_DONE = 'configure.py' # 删除此行可重新触发配置向导") + lines.append("") + + # Mixin 配置 + names = [c['name'] for c in llm_cfgs] + lines.append("# ── Mixin 故障转移 ──────────────────────────────────────────────────────────") + lines.append("mixin_config = {") + lines.append(f" 'llm_nos': {names},") + lines.append(" 'max_retries': 10,") + lines.append(" 'base_delay': 0.5,") + lines.append("}") + lines.append("") + + # 各模型配置 + type_counts = {} + for cfg in llm_cfgs: + cfg_type = cfg.get('type', 'native_oai') + type_counts[cfg_type] = type_counts.get(cfg_type, 0) + 1 + + type_indices = {} + for i, cfg in enumerate(llm_cfgs): + cfg_type = cfg.get('type', 'native_oai') + var_prefix, session_type = _var_type_info(cfg) + idx = type_indices.get(cfg_type, 0) + type_indices[cfg_type] = idx + 1 + + if type_counts[cfg_type] > 1: + var_name = f"{var_prefix}_{idx}" + else: + var_name = var_prefix + + lines.append(f"# ── {cfg['name']} ({session_type}) ─────────────────────────────────────────────") + lines.append(f"{var_name} = {{") + _write_config_fields(lines, cfg) + lines.append("}") + lines.append("") + + # 平台配置 + if platform_configs: + lines.append("# ══════════════════════════════════════════════════════════════════════════════") + lines.append("# 聊天平台集成") + lines.append("# ══════════════════════════════════════════════════════════════════════════════") + lines.append("") + for pc in platform_configs: + for key, val in pc['config'].items(): + _write_platform_value(lines, key, val) + lines.append("") + + # 尾部 + lines.append("# ══════════════════════════════════════════════════════════════════════════════") + lines.append("# 配置完毕!运行: python agentmain.py (终端 REPL)") + if platform_configs: + for pc in platform_configs: + p = pc['platform'] + lines.append(f"# 或: python {p['file']} ({p['name']})") + lines.append("# ══════════════════════════════════════════════════════════════════════════════") + + return '\n'.join(lines) + +def _write_config_fields(lines, cfg): + """写入配置字典的键值对(缩进的 'key': value, 格式)""" + for key in ['name', 'type', 'apikey', 'apibase', 'model', 'api_mode', + 'fake_cc_system_prompt', 'thinking_type', 'thinking_budget_tokens', + 'reasoning_effort', 'max_tokens', 'max_retries', 'connect_timeout', + 'read_timeout', 'temperature', 'context_win', + 'proxy', 'user_agent', 'stream']: + if key not in cfg: + continue + val = cfg[key] + if isinstance(val, bool): + lines.append(f" '{key}': {str(val)},") + elif isinstance(val, (int, float)): + lines.append(f" '{key}': {val},") + elif isinstance(val, str): + lines.append(f" '{key}': '{val}',") + else: + lines.append(f" '{key}': {repr(val)},") + +def _write_platform_value(lines, key, val): + """写入顶级变量(平台配置等)""" + if isinstance(val, list): + if val: + lines.append(f"{key} = {repr(val)}") + else: + lines.append(f"{key} = [] # 允许所有用户") + elif isinstance(val, str): + lines.append(f"{key} = '{val}'") + else: + lines.append(f"{key} = {repr(val)}") + + +def _parse_existing_mykey(): + """解析已有 mykey.py,返回 (model_names, platform_infos) + + model_names: [str] — 模型名列表 + platform_infos: [{'id': str, 'vars': [{'key': str, 'val': ...}]}] — 平台信息 + 解析失败时返回 ([], []) + """ + if not os.path.exists(MYKPY_PATH): + return [], [] + + with open(MYKPY_PATH, 'r', encoding='utf-8') as f: + content = f.read() + + # 解析模型名 + model_names = [] + m = re.search(r"'llm_nos':\s*\[([^\]]*)\]", content) + if m: + model_names = re.findall(r"'([^']+)'", m.group(1)) + + # 先收集所有已知平台 env var key → 判断值类型 + all_env_var_keys = {} + platform_env_keys = {} # pid -> [var_key] + for p in PLATFORMS: + pid = p['id'] + platform_env_keys.setdefault(pid, []) + for var in p.get('env_vars', []): + vkey = var['key'] + all_env_var_keys[vkey] = var + platform_env_keys[pid].append(vkey) + + # 逐平台解析所有已知变量 + platform_infos = [] + for pid, env_keys in platform_env_keys.items(): + vars_found = [] + for vkey in env_keys: + var_def = all_env_var_keys[vkey] + val = None + if var_def.get('is_list'): + # 匹配 `xxx = [...]` + m_var = re.search(rf"^{vkey}\s*=\s*(\[[^\]]*\])", content, re.MULTILINE) + if m_var: + try: + val = ast.literal_eval(m_var.group(1)) + except (ValueError, SyntaxError): + pass + else: + # 匹配 `xxx = '...'` + m_var = re.search(rf"^{vkey}\s*=\s*'([^']*)'", content, re.MULTILINE) + if m_var: + val = m_var.group(1) + if val is not None: + vars_found.append({'key': vkey, 'val': val}) + if vars_found: + platform_infos.append({'id': pid, 'vars': vars_found}) + + return model_names, platform_infos + + +def _parse_existing_llm_cfgs(): + """解析已有 mykey.py,返回完整 LLM 配置字典列表 [{name, apikey, ...}] + 解析失败时返回 [] + """ + if not os.path.exists(MYKPY_PATH): + return [] + + with open(MYKPY_PATH, 'r', encoding='utf-8') as f: + content = f.read() + + cfgs = [] + # 匹配所有 `xxx = { ... }` 顶层字典赋值 + # 用简单状态机: 找 `\w+ = {` 然后匹配花括号 + pattern = re.compile(r'^(\w+)\s*=\s*\{', re.MULTILINE) + for m in pattern.finditer(content): + brace_start = m.end() - 1 # '{' 的位置 + depth = 1 + i = brace_start + 1 + while i < len(content) and depth > 0: + if content[i] == '{': + depth += 1 + elif content[i] == '}': + depth -= 1 + i += 1 + if depth == 0: + dict_text = content[m.end():i - 1] + try: + d = ast.literal_eval('{' + dict_text + '}') + if isinstance(d, dict) and 'name' in d: + cfgs.append(d) + except (ValueError, SyntaxError): + continue + + return cfgs + + +def _backup_with_name(model_names, platform_ids): + """按 mykey+模型名+机器人名 格式备份旧 mykey.py""" + parts = ['mykey'] + for m in model_names[:3]: + parts.append(m.replace('/', '-').replace('\\', '-')) + for pid in platform_ids: + pid_clean = pid.replace('_', '') + if pid_clean not in parts: + parts.append(pid_clean) + safe_name = '_'.join(parts) + if safe_name == 'mykey': + safe_name = 'mykey_backup' # 避免和源文件同名 + if len(safe_name) > 100: + safe_name = safe_name[:100] + backup_path = os.path.join(PROJECT_ROOT, f'{safe_name}.py') + shutil.copy2(MYKPY_PATH, backup_path) + return backup_path + + +# ═══════════════════════════════════════════════════════════════════════════ +# Main +# ═══════════════════════════════════════════════════════════════════════════ + +def main(): + banner() + + # Python 版本检查 + ok, msg = _check_python() + if not ok: + print(f" {C['red']}✗ {msg}{C['reset']}") + sys.exit(1) + if msg: + color = 'yellow' if '⚠' in msg else 'green' + print(f" {C[color]}{msg}{C['reset']}\n") + + # ── 决策流程 ── + llm_cfgs = [] + platform_configs = [] + platform_deps = set() + is_modify = False + is_new = False + + if os.path.exists(MYKPY_PATH): + model_names, platform_infos = _parse_existing_mykey() + cur_models = ', '.join(model_names) if model_names else '(未知)' + cur_platforms = ', '.join(p['id'] for p in platform_infos) if platform_infos else '(无)' + print(f" {C['dim']} 当前: 模型=[{cur_models}], 平台=[{cur_platforms}]{C['reset']}") + + mode = ask_choice( + "检测到已有 mykey.py,请选择操作", + [ + {'id': 'modify', 'name': '修改现有配置', 'desc': '保留未改部分,只重新配置选定项'}, + {'id': 'new', 'name': '新建配置(备份旧文件)', 'desc': '备份为 mykey+模型+平台.py,然后全新配置'}, + ], + default=None, + ) + + if mode == 'new': + backup_path = _backup_with_name(model_names, [p['id'] for p in platform_infos]) + print(f" {C['green']}✓ 旧配置已备份至:{C['reset']} {C['dim']}{backup_path}{C['reset']}") + is_new = True + else: + is_modify = True + scope = ask_choice( + "你要修改什么?", + [ + {'id': 'both', 'name': '两项都重新配置', 'desc': 'LLM + 平台全部更新'}, + {'id': 'llm', 'name': '重新配置 LLM 模型', 'desc': f'当前: {cur_models}'}, + {'id': 'platform', 'name': '重新配置消息平台', 'desc': f'当前: {cur_platforms}'}, + ], + ) + if scope in ('llm', 'both'): + llm_cfgs = _do_llm() + if scope in ('platform', 'both'): + platform_configs, platform_deps = configure_platforms() + if scope == 'llm' and platform_infos: + for pi in platform_infos: + p = next((x for x in PLATFORMS if x['id'] == pi['id']), None) + if p: + config_dict = {v['key']: v['val'] for v in pi['vars']} + platform_configs.append({'platform': p, 'config': config_dict}) + elif scope == 'platform' and model_names: + old_cfgs = _parse_existing_llm_cfgs() + if old_cfgs: + llm_cfgs = old_cfgs + print(f"\n {C['green']}✓ 已保留现有 LLM 配置: {', '.join(c['name'] for c in old_cfgs)}{C['reset']}") + else: + print(f"\n {C['yellow']}⚠ 保留 LLM 配置失败,将生成空配置。建议两项都重新配置。{C['reset']}") + + if not is_modify: + if is_new: + hint = "已备份旧配置,请完成全新设置" + else: + hint = "首次配置,建议同时设置模型和消息平台" + print(f" {C['dim']} {hint}。{C['reset']}") + + scope = ask_choice( + "你想配置什么?", + [ + {'id': 'both', 'name': '两项都配置 (推荐)', 'desc': 'LLM 模型 + 消息平台,完整初始化'}, + {'id': 'llm', 'name': '仅 LLM 模型', 'desc': '只配置模型,稍后再配平台'}, + {'id': 'platform', 'name': '仅消息平台', 'desc': '只配平台,稍后再配模型'}, + ], + default='both', + ) + + if scope in ('llm', 'both'): + llm_cfgs = _do_llm() + if scope == 'llm': + if ask_yesno("是否继续配置消息平台?", default=True): + platform_configs, platform_deps = configure_platforms() + + if scope == 'both': + platform_configs, platform_deps = configure_platforms() + + if scope == 'platform': + platform_configs, platform_deps = configure_platforms() + if ask_yesno("是否继续配置 LLM 模型?", default=True): + llm_cfgs = _do_llm() + elif os.path.exists(MYKPY_PATH): + # 新建+仅平台:从备份保留旧 LLM 配置 + old_cfgs = _parse_existing_llm_cfgs() + if old_cfgs: + llm_cfgs = old_cfgs + print(f"\n {C['green']}✓ 已保留备份中的 LLM 配置: {', '.join(c['name'] for c in old_cfgs)}{C['reset']}") + + # ── 生成 mykey.py ── + if not llm_cfgs and not platform_configs: + print(f"\n {C['yellow']}⚠ 没有配置任何内容,退出。{C['reset']}") + sys.exit(0) + + content = generate_mykey(llm_cfgs, platform_configs) + + # 备份旧文件(修改模式不备份,直接在原文件修改) + if os.path.exists(MYKPY_PATH) and not is_modify and not is_new: + backup = _backup_with_name(model_names, [p['id'] for p in platform_infos]) + print(f"\n {C['green']}✓ 旧配置已备份至:{C['reset']} {C['dim']}{backup}{C['reset']}") + + # 写入 + with open(MYKPY_PATH, 'w', encoding='utf-8') as f: + f.write(content) + print(f"\n {C['green']}✓ mykey.py 已生成!{C['reset']}") + + # ── 完成提示 ── + print(f"\n{C['bold']}{C['green']}╔══════════════════════════════════════╗") + print(f"║ 配置完成! ║") + print(f"╚══════════════════════════════════════╝{C['reset']}") + print() + if llm_cfgs: + print(f" {C['cyan']} 终端 REPL:{C['reset']} python agentmain.py") + if platform_configs: + for i, pc in enumerate(platform_configs, 1): + p = pc['platform'] + print(f" {C['cyan']} 平台 {i} ({p['name']}):{C['reset']} python {p['file']}") + print() + + # pip 依赖提示 + all_deps = sorted(platform_deps) + if all_deps: + print(f" {C['yellow']}💡 提示:你需要安装以下依赖以使消息平台正常工作:{C['reset']}") + print(f" {C['cyan']}pip install {' '.join(all_deps)}{C['reset']}") + print() + + # ── 入门示例 ── + print(f" {C['bold']}试试这些命令:{C['reset']}") + examples = [ + "帮我在桌面创建一个 hello.txt,内容是 Hello World", + "请查看你的代码,安装所有用得上的 python 依赖", + "执行 web setup sop,解锁 web 工具", + "打开淘宝,搜索 iPhone 16,按价格排序", + "用rapidocr配置你的ocr能力并存入记忆", + "git 更新你的代码,然后看看 commit 有什么新功能", + "把这个记到你的记忆里", + ] + for ex in examples: + print(f" {C['dim']}{ex}{C['reset']}") + print() + + print(f" {C['green']}{C['bold']}合抱之木,生于毫末{C['reset']}\n") + + +def _do_llm(): + """配置 LLM 模型,失败则 exit。""" + cfgs = configure_llms() + if not cfgs: + print(f"\n {C['red']}✗ 至少需要配置一个模型才能使用。退出。{C['reset']}") + sys.exit(1) + return cfgs + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + print(f"\n\n {C['yellow']}⚠ 用户中断{C['reset']}") + sys.exit(0) diff --git a/assets/cookie_grabber/background.js b/assets/cookie_grabber/background.js deleted file mode 100644 index cdff0aa7c..000000000 --- a/assets/cookie_grabber/background.js +++ /dev/null @@ -1,24 +0,0 @@ -// background.js - 保留原有事件 + 新增消息监听 -chrome.runtime.onInstalled.addListener(() => { - console.log('Cookie Grabber installed'); -}); - -// content script 请求cookie时的处理 -chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { - if (msg.type === 'getCookies' && sender.tab) { - const url = sender.tab.url; - // 普通cookie + partitioned cookie 双查合并 - chrome.cookies.getAll({url}, cookies => { - console.log('[CookieGrabber] normal cookies:', cookies.map(c => c.name)); - chrome.cookies.getAll({url, partitionKey: {topLevelSite: url.match(/^https?:\/\/[^/]+/)[0]}}, pCookies => { - console.log('[CookieGrabber] partitioned cookies:', pCookies.map(c => c.name)); - const map = {}; - cookies.forEach(c => map[c.name] = c.value); - pCookies.forEach(c => map[c.name] = c.value); - const str = Object.entries(map).map(([k,v]) => k + '=' + v).join('; '); - sendResponse({cookies: str}); - }); - }); - return true; // 异步响应 - } -}); \ No newline at end of file diff --git a/assets/cookie_grabber/content.js b/assets/cookie_grabber/content.js deleted file mode 100644 index 64ad502dc..000000000 --- a/assets/cookie_grabber/content.js +++ /dev/null @@ -1,18 +0,0 @@ -// content.js - MutationObserver 监听触发元素 -const TRIGGER_ID = '__ljqcg__'; - -const obs = new MutationObserver(muts => { - for (const m of muts) { - for (const node of m.addedNodes) { - if (node.nodeType === 1 && node.id === TRIGGER_ID) { - chrome.runtime.sendMessage({type: 'getCookies'}, res => { - if (res && res.cookies) node.textContent = res.cookies; - else node.textContent = '__cg_error__'; - }); - return; - } - } - } -}); - -obs.observe(document.documentElement, {childList: true, subtree: true}); diff --git a/assets/cookie_grabber/manifest.json b/assets/cookie_grabber/manifest.json deleted file mode 100644 index 1e41399bd..000000000 --- a/assets/cookie_grabber/manifest.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "manifest_version": 3, - "name": "Cookie Grabber", - "version": "1.0", - "description": "获取所有 cookies", - "permissions": [ - "cookies", - "storage", - "tabs", - "activeTab" - ], - "background": { - "service_worker": "background.js" - }, - "action": { - "default_popup": "popup.html" - }, - "host_permissions": [ - "" - ], - "content_scripts": [ - { - "matches": [ - "" - ], - "js": [ - "content.js" - ], - "run_at": "document_idle" - } - ] -} \ No newline at end of file diff --git a/assets/cookie_grabber/popup.html b/assets/cookie_grabber/popup.html deleted file mode 100644 index b25f8a49d..000000000 --- a/assets/cookie_grabber/popup.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - HttpOnly Cookie Grabber - - - - -

Get All Cookies

-
    No available cookies
- - - \ No newline at end of file diff --git a/assets/cookie_grabber/popup.js b/assets/cookie_grabber/popup.js deleted file mode 100644 index 397943518..000000000 --- a/assets/cookie_grabber/popup.js +++ /dev/null @@ -1,56 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - // 当刷新按钮被点击时,获取当前页面的 cookies - document.getElementById('refresh').addEventListener('click', fetchCookies); - // 加载时显示当前页面的 cookies - fetchCookies(); -}); - -// 从 cookies 存储中获取当前页面的 cookies -function fetchCookies() { - // 获取当前活动的标签页 - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - const activeTab = tabs[0]; // 获取活动标签页 - const currentUrl = activeTab.url; // 当前页面的 URL - console.log("当前活动的 URL:", currentUrl); - - // 双查: 普通 + partitioned - const origin = currentUrl.match(/^https?:\/\/[^\/]+/)[0]; - chrome.cookies.getAll({ url: currentUrl }, (cookies) => { - chrome.cookies.getAll({ url: currentUrl, partitionKey: { topLevelSite: origin } }, (pCookies) => { - const map = {}; - cookies.forEach(c => map[c.name] = c.value); - pCookies.forEach(c => map[c.name] = c.value); - const allCookies = Object.entries(map); - - const cookiesDisplay = document.getElementById('cookiesDisplay'); - cookiesDisplay.innerHTML = ''; - - if (allCookies.length === 0) { - cookiesDisplay.innerHTML = '
  • No available cookies
  • '; - } else { - let cookiesString = ''; - allCookies.forEach(([name, value]) => { - const li = document.createElement('li'); - li.textContent = `${name}: ${value}`; - cookiesDisplay.appendChild(li); - cookiesString += `${name}=${value}; `; - }); - console.log('cookies:', allCookies.length); - copyCookiesToClipboard(cookiesString.trim()); - } - }); - }); - }); -} - -// 将 cookies 复制到剪贴板 -function copyCookiesToClipboard(cookiesString) { - // 使用 Clipboard API 复制到剪贴板 - navigator.clipboard.writeText(cookiesString) - .then(() => { - console.log("Cookies copied to clipboard:", cookiesString); - }) - .catch(err => { - console.error("Unable to copy to clipboard:", err); - }); -} \ No newline at end of file diff --git a/assets/demo/codeg-demo.gif b/assets/demo/codeg-demo.gif new file mode 100644 index 000000000..cd120effb Binary files /dev/null and b/assets/demo/codeg-demo.gif differ diff --git a/assets/demo/discord_hcaptcha_real_browser.gif b/assets/demo/discord_hcaptcha_real_browser.gif new file mode 100644 index 000000000..5630e8cda Binary files /dev/null and b/assets/demo/discord_hcaptcha_real_browser.gif differ diff --git a/assets/ga_httpapp.py b/assets/ga_httpapp.py new file mode 100644 index 000000000..c479e9cbb --- /dev/null +++ b/assets/ga_httpapp.py @@ -0,0 +1,125 @@ +import threading, sys, os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from fastapi import FastAPI, Header, HTTPException, Query, Depends; from fastapi.responses import HTMLResponse +from pydantic import BaseModel +from agentmain import GenericAgent as GA + +PORT, API_KEY = int(sys.argv[1]), sys.argv[2] +app, agent, lock = FastAPI(), GA(), threading.Lock() +outputs, stopped = [], True +threading.Thread(target=agent.run, daemon=True).start() +class Req(BaseModel): prompt: str = "" +agent.verbose = False + +def require_key(key: str = Query(None), x_api_key: str = Header(None, alias="X-API-Key")): + if API_KEY not in (key, x_api_key): raise HTTPException(404) + +def run_task(prompt): + global stopped + segs = [] # 本任务按 turn 索引的分段输出 + with lock: task_start = len(outputs) + def flush(): + with lock: outputs[task_start:] = segs + try: + dq = agent.put_task(prompt, source="http") + while "done" not in (item := dq.get(timeout=2200)): + outs = item.get("outputs") + if not outs: continue + idx = max(0, int(item.get("turn", 0) or 0) - 1) # turn 1-based → 槽位 0-based + while len(segs) <= idx: segs.append("") + segs[idx] = str(outs[-1]) # 当前 turn + if len(outs) >= 2 and idx >= 1: segs[idx - 1] = str(outs[-2]) # 前一 turn 落定值 + flush() + segs = [str(s) for s in item.get("outputs", [])] # done 时全量替换 + flush() + finally: stopped = True + +@app.post("/put_task") +def put_task(req: Req, _=Depends(require_key)): + global stopped + with lock: + if not stopped: return {"ok": False, "error": "should abort first"} + stopped = False + threading.Thread(target=run_task, args=(req.prompt,), daemon=True).start() + return {"ok": True} + +@app.post("/abort") +def abort(_=Depends(require_key)): agent.abort(); return {"ok": True} + +@app.post("/input") +def input_task(req: Req, _=Depends(require_key)): + global stopped + if not stopped: agent.intervene = req.prompt; return {"ok": True, "mode": "intervene"} + with lock: + if not stopped: agent.intervene = req.prompt; return {"ok": True, "mode": "intervene"} + stopped = False + threading.Thread(target=run_task, args=(req.prompt,), daemon=True).start() + return {"ok": True, "mode": "task"} + +@app.get("/output") +def get_output(k: int = Query(5), _=Depends(require_key)): + with lock: r = outputs[-k:] + return {"stopped": stopped, "output": "\n".join(r), + "history": "\n".join(str(h) for h in agent.history)} + +@app.get("/llm") +def llm_ep(llm_no: int = Query(None), _=Depends(require_key)): + if llm_no is not None: + agent.next_llm(llm_no) + return {"llm_no": agent.llm_no, "name": agent.get_llm_name(), + "llms": [{"no": i, "name": n, "current": a} for i, n, a in agent.list_llms()]} + +@app.get("/sysprompt") +def sysprompt_ep(text: str = Query(None), _=Depends(require_key)): + if text is not None: + agent.extra_sys_prompts = [text] if text else [] + return {"extra_sys_prompts": agent.extra_sys_prompts} + +HELP = """GA HTTP 操作协议(所有请求带 ?key=API_KEY,或 Header X-API-Key) +GET /output?k=N 查看状态:{stopped, output(末N条), history}。stopped=true 表示空闲 +POST /input {prompt} 下发指令:空闲时作为新任务,忙时作为中途干预(intervene) +POST /abort 中止当前任务 +GET /llm[?llm_no=N] 查/切模型:返回 {llm_no,name,llms:[{no,name,current}]} +GET /sysprompt[?text]查/设附加系统提示(extra_sys_prompts),text 为空则清空 +纠偏流程:先 GET /output 读 history 判断状态→需要时 POST /input 注入纠偏指令""" + +@app.get("/help") +def help_ep(_=Depends(require_key)): return {"help": HELP} + +@app.get("/") +def ui(): + return HTMLResponse(f""" + +GA Monitor + + +
    ● Loading...
    +

    Output

    +

    History

    + + +""") + +if __name__ == "__main__": + import uvicorn; uvicorn.run(app, host="0.0.0.0", port=PORT) diff --git a/assets/ga_install.ps1 b/assets/ga_install.ps1 new file mode 100644 index 000000000..5d5e19501 --- /dev/null +++ b/assets/ga_install.ps1 @@ -0,0 +1,577 @@ +#requires -version 5.1 + +<#! + +GenericAgent one-click portable deployer for Windows. + + + +Modes: + + Default/Mainland: download GenericAgent.zip + uv + PortableGit from user's VPS, set China PyPI mirror. + + GLOBAL=1: clone GenericAgent from GitHub; uv and PortableGit also come from GitHub releases; no PyPI mirror. + + + +Portable components are installed under \.portable: + + uv, Python installed by uv, PortableGit. + +#> + +param( + + [string]$InstallDir = "$env:USERPROFILE\GenericAgent", + + [string]$PythonVersion = "3.12", + + [switch]$Force + +) + + + +$ErrorActionPreference = "Stop" + + + +# Make Chinese output reliable in Windows PowerShell 5.1 and redirected logs. + +try { + + [Console]::InputEncoding = [System.Text.Encoding]::UTF8 + + [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + + $OutputEncoding = [System.Text.Encoding]::UTF8 + +} catch { } + + + +$RepoUrl = "https://github.com/lsdefine/GenericAgent.git" + +$VpsBase = "http://47.101.182.29:9000" + +$GaZipUrl = "$VpsBase/files/GenericAgent.zip" + +$UvUrl = "$VpsBase/uv/uv-x86_64-pc-windows-msvc.zip" + +$GitUrl = "$VpsBase/files/PortableGit-2.54.0-64-bit.7z.exe" + +$Deps = @("requests>=2.28", "beautifulsoup4>=4.12", "bottle>=0.12", "simple-websocket-server>=0.4", "streamlit>=1.28") + +$MainlandIndex = "https://pypi.tuna.tsinghua.edu.cn/simple" + +$GlobalMode = ($env:GLOBAL -eq "1") + +if ($GlobalMode) { + # GLOBAL=1: fetch everything from GitHub; no mainland endpoints involved. + $UvUrl = "https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-pc-windows-msvc.zip" + $GitUrl = "https://github.com/git-for-windows/git/releases/download/v2.54.0.windows.1/PortableGit-2.54.0-64-bit.7z.exe" +} + + + +$GaDir = [IO.Path]::GetFullPath($InstallDir) + +$PortableRoot = Join-Path $GaDir ".portable" + +$Bin = Join-Path $PortableRoot "bin" + +$Cache = Join-Path $PortableRoot "cache" + +$Tools = Join-Path $PortableRoot "tools" + +$UvZip = Join-Path $Cache "uv-x86_64-pc-windows-msvc.zip" + +$GaZip = Join-Path $Cache "GenericAgent.zip" + +$GitExeArchive = Join-Path $Cache "PortableGit-2.54.0-64-bit.7z.exe" + +$UvExtract = Join-Path $Cache "uv-extract" + +$GaExtract = Join-Path $Cache "ga-extract" + +$GitDir = Join-Path $Tools "PortableGit" + +$UvExe = Join-Path $Bin "uv.exe" + +$GitExe = Join-Path $GitDir "bin\git.exe" + +$EnvCmd = Join-Path $GaDir "env.cmd" + +$EnvPs1 = Join-Path $GaDir "env.ps1" + + + +function Say($m) { Write-Host "[ga-deploy] $m" -ForegroundColor Cyan } + +function Ok($m) { Write-Host "[ok] $m" -ForegroundColor Green } + +function Die($m) { Write-Host "[error] $m" -ForegroundColor Red; exit 1 } + +function Invoke-Native([scriptblock]$Command) { + + $prevEAP = $ErrorActionPreference + + $ErrorActionPreference = 'Continue' + + try { & $Command } finally { $ErrorActionPreference = $prevEAP } + + return $LASTEXITCODE + +} + + + +function Download-File($Url, $OutFile) { + + New-Item -ItemType Directory -Force -Path (Split-Path $OutFile) | Out-Null + + Say "Downloading $Url" + + $wc = New-Object System.Net.WebClient + + $wc.Headers.Add("User-Agent", "Mozilla/5.0 ga-deploy") + + try { $wc.DownloadFile($Url, $OutFile) } finally { $wc.Dispose() } + + if (!(Test-Path $OutFile) -or ((Get-Item $OutFile).Length -lt 1024)) { Die "Download failed: $Url" } + +} + + + +function Expand-ZipClean($Zip, $Dest) { + + if (Test-Path $Dest) { Remove-Item -Recurse -Force $Dest } + + New-Item -ItemType Directory -Force -Path $Dest | Out-Null + + Expand-Archive -Path $Zip -DestinationPath $Dest -Force + +} + + + +function Copy-DirectoryContents($Src, $Dst) { + + New-Item -ItemType Directory -Force -Path $Dst | Out-Null + + Get-ChildItem -LiteralPath $Src -Force | ForEach-Object { + + Copy-Item -LiteralPath $_.FullName -Destination $Dst -Recurse -Force + + } + +} + + + +Say "Install dir: $GaDir" + +Say "Mode: $(if ($GlobalMode) { 'GLOBAL=1 / GitHub clone' } else { 'Mainland / VPS zip' })" + + + +if ((Test-Path $GaDir) -and $Force) { Remove-Item -Recurse -Force $GaDir } + +New-Item -ItemType Directory -Force -Path $GaDir,$PortableRoot,$Bin,$Cache,$Tools | Out-Null + + + +# uv (GitHub release in GLOBAL mode, user's VPS otherwise) + +if (!(Test-Path $UvExe) -or $Force) { + + Download-File $UvUrl $UvZip + + Expand-ZipClean $UvZip $UvExtract + + $foundUv = Get-ChildItem -Path $UvExtract -Recurse -Filter "uv.exe" | Select-Object -First 1 + + if (!$foundUv) { Die "uv.exe not found in archive" } + + Copy-Item $foundUv.FullName $UvExe -Force + +} + +Ok "uv: $(& $UvExe --version)" + + + +# Configure portable Python location. Mirror only in mainland mode. + +$env:UV_PYTHON_INSTALL_DIR = Join-Path $PortableRoot "uv-python" + +$env:UV_CACHE_DIR = Join-Path $PortableRoot "uv-cache" + +if ($GlobalMode) { + + Remove-Item Env:UV_DEFAULT_INDEX -ErrorAction SilentlyContinue + + Remove-Item Env:PIP_INDEX_URL -ErrorAction SilentlyContinue + +} else { + + $env:UV_DEFAULT_INDEX = $MainlandIndex + + $env:PIP_INDEX_URL = $MainlandIndex + +} + +$env:PATH = "$Bin;$env:PATH" + + + + +# Workaround: uv creates minor-version symlinks (junctions) in UV_PYTHON_INSTALL_DIR. +# If a previous interrupted install left a plain directory, uv fails with os error 4390. +# Fix: remove any non-junction subdirectory so uv can recreate them cleanly. +$uvPyDir = $env:UV_PYTHON_INSTALL_DIR +if (Test-Path $uvPyDir) { + Get-ChildItem -LiteralPath $uvPyDir -Directory | ForEach-Object { + $attr = $_.Attributes + if (($attr -band [IO.FileAttributes]::ReparsePoint) -eq 0) { + Say "Removing stale non-junction dir: $($_.Name)" + Remove-Item -LiteralPath $_.FullName -Recurse -Force + } + } +} + +Say "Installing Python $PythonVersion via uv" + +$ec = Invoke-Native { & $UvExe python install $PythonVersion } + +if ($ec -ne 0) { Die "uv python install failed" } + +$PythonExe = (& $UvExe python find $PythonVersion).Trim() + +if (!(Test-Path $PythonExe)) { Die "uv installed Python but python.exe was not found" } + +Ok "Python: $(& $PythonExe --version)" + + + +# PortableGit (GitHub release in GLOBAL mode, user's VPS otherwise). Needed for GLOBAL=1 and useful for user shell. + +if (!(Test-Path $GitExe) -or $Force) { + + Download-File $GitUrl $GitExeArchive + + if (Test-Path $GitDir) { Remove-Item -Recurse -Force $GitDir } + + New-Item -ItemType Directory -Force -Path $GitDir | Out-Null + + Say "Extracting PortableGit" + + $ec = Invoke-Native { & $GitExeArchive -y -o"$GitDir" | Out-Null } + + if ($ec -ne 0) { Die "PortableGit extraction failed" } + +} + +if (!(Test-Path $GitExe)) { Die "git.exe missing: $GitExe" } + +Ok "Git: $(& $GitExe --version)" + + + +$PythonDir = Split-Path $PythonExe -Parent + +$GitBin = Split-Path $GitExe -Parent + +$GitUsrBin = Join-Path $GitDir "usr\bin" + +$env:PATH = "$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;$env:PATH" + + + +# Fetch/update GenericAgent source. + +if ($GlobalMode) { + + Say "Cloning GenericAgent from GitHub" + + $items = @(Get-ChildItem -LiteralPath $GaDir -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne ".portable" }) + + if ($items.Count -gt 0) { + + if (!$Force) { Die "Install dir contains files. Re-run with -Force to replace source while preserving portable tools." } + + $items | Remove-Item -Recurse -Force + + } + + $TmpClone = Join-Path $Cache "ga-clone" + + if (Test-Path $TmpClone) { Remove-Item -Recurse -Force $TmpClone } + + $ec = Invoke-Native { & $GitExe clone --depth 1 $RepoUrl $TmpClone } + + if ($ec -ne 0) { Die "git clone failed" } + + Copy-DirectoryContents $TmpClone $GaDir + + Remove-Item -Recurse -Force $TmpClone + +} else { + + Say "Downloading GenericAgent package from VPS" + + Download-File $GaZipUrl $GaZip + + Expand-ZipClean $GaZip $GaExtract + + $SrcDir = Join-Path $GaExtract "GenericAgent" + + if (!(Test-Path $SrcDir)) { $SrcDir = $GaExtract } + + $items = @(Get-ChildItem -LiteralPath $GaDir -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne ".portable" }) + + if ($items.Count -gt 0) { $items | Remove-Item -Recurse -Force } + + Copy-DirectoryContents $SrcDir $GaDir + +} + +Ok "GenericAgent source ready: $GaDir" + + + +# Install basic dependencies and project in editable mode into portable Python. + +Say "Installing GenericAgent dependencies via uv pip" + +$installArgs = @("pip", "install", "--break-system-packages", "--python", $PythonExe) + +if (!$GlobalMode) { $installArgs += @("--index-url", $MainlandIndex) } + +$installArgs += $Deps + +$ec = Invoke-Native { & $UvExe @installArgs } + +if ($ec -ne 0) { Die "dependency install failed" } + + + +if (Test-Path (Join-Path $GaDir "pyproject.toml")) { + + $projectArgs = @("pip", "install", "--break-system-packages", "--python", $PythonExe) + + if (!$GlobalMode) { $projectArgs += @("--index-url", $MainlandIndex) } + + $projectArgs += @("-e", $GaDir) + + $ec = Invoke-Native { & $UvExe @projectArgs } + + if ($ec -ne 0) { Die "editable project install failed" } + +} + + + +# Try-install pywebview (optional UI). Failure is non-fatal. + +Say "Attempting to install pywebview (optional, failure is OK)" + +$webviewArgs = @("pip", "install", "--break-system-packages", "--python", $PythonExe) + +if (!$GlobalMode) { $webviewArgs += @("--index-url", $MainlandIndex) } + +$webviewArgs += @("pywebview>=4.0") + +$ec = Invoke-Native { & $UvExe @webviewArgs 2>&1 | Out-Null } + +if ($ec -ne 0) { + + Write-Host "[warn] pywebview install failed. This is optional." -ForegroundColor Yellow + + Write-Host " On Windows it usually works out of the box." -ForegroundColor Yellow + + Write-Host " If needed later: uv pip install pywebview" -ForegroundColor Yellow + +} else { + + Ok "pywebview installed successfully" + +} + + + +# Activation scripts: portable paths are intentionally before system PATH. + +if ($GlobalMode) { + +@" + +@echo off + +set "PORTABLE_DEV_ROOT=$PortableRoot" + +set "GENERICAGENT_HOME=$GaDir" + +set "UV_PYTHON_INSTALL_DIR=$PortableRoot\uv-python" + +set "UV_CACHE_DIR=$PortableRoot\uv-cache" + +set "PATH=$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;%PATH%" + +echo Activated GenericAgent portable env: %GENERICAGENT_HOME% + +"@ | Set-Content -Path $EnvCmd -Encoding ASCII + + + +@" + +`$env:PORTABLE_DEV_ROOT = "$PortableRoot" + +`$env:GENERICAGENT_HOME = "$GaDir" + +`$env:UV_PYTHON_INSTALL_DIR = "$PortableRoot\uv-python" + +`$env:UV_CACHE_DIR = "$PortableRoot\uv-cache" + +`$env:PATH = "$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;`$env:PATH" + +Write-Host "Activated GenericAgent portable env: `$env:GENERICAGENT_HOME" -ForegroundColor Green + +"@ | Set-Content -Path $EnvPs1 -Encoding UTF8 + +} else { + +@" + +@echo off + +set "PORTABLE_DEV_ROOT=$PortableRoot" + +set "GENERICAGENT_HOME=$GaDir" + +set "UV_PYTHON_INSTALL_DIR=$PortableRoot\uv-python" + +set "UV_CACHE_DIR=$PortableRoot\uv-cache" + +set "UV_DEFAULT_INDEX=$MainlandIndex" + +set "PIP_INDEX_URL=$MainlandIndex" + +set "PATH=$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;%PATH%" + +echo Activated GenericAgent portable env: %GENERICAGENT_HOME% + +"@ | Set-Content -Path $EnvCmd -Encoding ASCII + + + +@" + +`$env:PORTABLE_DEV_ROOT = "$PortableRoot" + +`$env:GENERICAGENT_HOME = "$GaDir" + +`$env:UV_PYTHON_INSTALL_DIR = "$PortableRoot\uv-python" + +`$env:UV_CACHE_DIR = "$PortableRoot\uv-cache" + +`$env:UV_DEFAULT_INDEX = "$MainlandIndex" + +`$env:PIP_INDEX_URL = "$MainlandIndex" + +`$env:PATH = "$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;`$env:PATH" + +Write-Host "Activated GenericAgent portable env: `$env:GENERICAGENT_HOME" -ForegroundColor Green + +"@ | Set-Content -Path $EnvPs1 -Encoding UTF8 + +} + + + +Ok "Verification:" + +& $UvExe --version + +& $PythonExe --version + +& $GitExe --version + +& $PythonExe -c "import requests, bs4, bottle; print('deps ok')" + +Write-Host "" + + + +# Copy mykey template if mykey.py does not exist (GLOBAL mode only) + +$MykeyDst = Join-Path $GaDir "mykey.py" + +if ($GlobalMode -and !(Test-Path $MykeyDst)) { + + $MykeyTpl = Join-Path $GaDir "mykey_template_en.py" + + if (Test-Path $MykeyTpl) { + + Copy-Item $MykeyTpl $MykeyDst + + Ok "Copied mykey_template_en.py -> mykey.py" + + } + +} + + + +# Final banner + +Write-Host "" + +if ($GlobalMode) { + + Write-Host @" + +╔═══════════════════════════════════════════════╗ + +║ ✅ GenericAgent installed successfully! ║ + +╠═══════════════════════════════════════════════╣ + +║ 📁 Location: $GaDir + +║ 🔑 Config: edit mykey.py (copied from template) + +║ 🚀 Launch: ga tui / ga launch / ga hub + +╚═══════════════════════════════════════════════╝ + +"@ + +} else { + + Write-Host @" + +╔═══════════════════════════════════════════════╗ + +║ [OK] GenericAgent 安装完成! ║ + +╠═══════════════════════════════════════════════╣ + +║ 安装目录: $GaDir + +║ 配置密钥: ga configure + +║ 启动: ga tui / ga launch / ga hub + +╚═══════════════════════════════════════════════╝ + +"@ + +} + +Write-Host "" + +Write-Host " Activate env: cmd.exe → call `"$EnvCmd`" | PowerShell → . `"$EnvPs1`"" + diff --git a/assets/ga_install.sh b/assets/ga_install.sh new file mode 100644 index 000000000..df6ad3227 --- /dev/null +++ b/assets/ga_install.sh @@ -0,0 +1,289 @@ +#!/usr/bin/env bash +set -euo pipefail + +# GenericAgent one-click portable deployer for macOS/Linux. +# Modes: +# Default/Mainland: download GenericAgent.zip + uv from user's VPS, set China PyPI mirror. +# GLOBAL=1: clone GenericAgent from GitHub; uv also from GitHub releases; no PyPI mirror. +# Portable components are installed under /.portable: +# uv, Python installed by uv. On macOS/Linux git is expected from system package manager. + +INSTALL_DIR="${INSTALL_DIR:-$HOME/GenericAgent}" +PYTHON_VERSION="${PYTHON_VERSION:-3.12}" +FORCE="${FORCE:-0}" +GLOBAL="${GLOBAL:-0}" + +REPO_URL="https://github.com/lsdefine/GenericAgent.git" +VPS_BASE="http://47.101.182.29:9000" +GA_ZIP_URL="$VPS_BASE/files/GenericAgent.zip" +MAINLAND_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple" +DEPS=("requests>=2.28" "beautifulsoup4>=4.12" "bottle>=0.12" "simple-websocket-server>=0.4" "streamlit>=1.28") + +say(){ printf '\033[36m[ga-deploy]\033[0m %s\n' "$*"; } +ok(){ printf '\033[32m[ok]\033[0m %s\n' "$*"; } +die(){ printf '\033[31m[error]\033[0m %s\n' "$*" >&2; exit 1; } + +usage(){ cat <<'EOF' +Usage: + bash install_portable_env.sh + INSTALL_DIR="$HOME/GenericAgent" PYTHON_VERSION=3.12 FORCE=1 bash install_portable_env.sh + GLOBAL=1 bash install_portable_env.sh + +Environment variables: + INSTALL_DIR Install GenericAgent here. Default: ~/GenericAgent + PYTHON_VERSION Python version installed by uv. Default: 3.12 + FORCE=1 Replace existing source files while preserving/reinstalling portable tools. + GLOBAL=1 Clone from GitHub directly and do not set China PyPI mirror. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then usage; exit 0; fi + +OS="$(uname -s)" +ARCH="$(uname -m)" +case "$OS:$ARCH" in + Darwin:x86_64) uv_file="uv-x86_64-apple-darwin.tar.gz" ;; + Darwin:arm64) uv_file="uv-aarch64-apple-darwin.tar.gz" ;; + Linux:x86_64) uv_file="uv-x86_64-unknown-linux-gnu.tar.gz" ;; + Linux:aarch64|Linux:arm64) uv_file="uv-aarch64-unknown-linux-gnu.tar.gz" ;; + *) die "Unsupported platform: $OS $ARCH" ;; +esac + +GA_DIR="${INSTALL_DIR/#\~/$HOME}" +case "$GA_DIR" in + /*) ;; + *) GA_DIR="$PWD/$GA_DIR" ;; +esac +mkdir -p "$GA_DIR" +GA_DIR="$(cd "$GA_DIR" && pwd -P)" +PORTABLE_ROOT="$GA_DIR/.portable" +BIN="$PORTABLE_ROOT/bin" +CACHE="$PORTABLE_ROOT/cache" +UV_TGZ="$CACHE/$uv_file" +GA_ZIP="$CACHE/GenericAgent.zip" +UV_EXTRACT="$CACHE/uv-extract" +GA_EXTRACT="$CACHE/ga-extract" +UV_EXE="$BIN/uv" +ENV_SH="$GA_DIR/env.sh" + +mkdir -p "$GA_DIR" "$PORTABLE_ROOT" "$BIN" "$CACHE" + +say "Install dir: $GA_DIR" +if [[ "$GLOBAL" == "1" ]]; then say "Mode: GLOBAL=1 / GitHub clone"; else say "Mode: Mainland / VPS zip"; fi + +if [[ "$FORCE" == "1" ]]; then + # Preserve .portable if present; remove source files later before deploying. + : +fi + +download(){ + local url="$1" out="$2" + mkdir -p "$(dirname "$out")" + say "Downloading $url" + if command -v curl >/dev/null 2>&1; then + curl -fL --retry 3 -A "ga-deploy" -o "$out" "$url" + elif command -v wget >/dev/null 2>&1; then + wget -O "$out" "$url" + else + die "curl or wget is required" + fi + [[ -s "$out" ]] || die "Download failed: $url" +} + +extract_tgz_clean(){ + local tgz="$1" dest="$2" + rm -rf "$dest"; mkdir -p "$dest" + tar -xzf "$tgz" -C "$dest" +} + +extract_zip_clean(){ + local zip="$1" dest="$2" + rm -rf "$dest"; mkdir -p "$dest" + if command -v unzip >/dev/null 2>&1; then + unzip -q "$zip" -d "$dest" + else + python3 - "$zip" "$dest" <<'PY' +import sys, zipfile +with zipfile.ZipFile(sys.argv[1]) as z: + z.extractall(sys.argv[2]) +PY + fi +} + +copy_contents(){ + local src="$1" dst="$2" + mkdir -p "$dst" + (cd "$src" && tar -cf - .) | (cd "$dst" && tar -xf -) +} + +remove_source_files(){ + shopt -s dotglob nullglob + for p in "$GA_DIR"/*; do + [[ "$(basename "$p")" == ".portable" ]] && continue + rm -rf "$p" + done + shopt -u dotglob nullglob +} + +# uv: GitHub release in GLOBAL mode, user's VPS otherwise +if [[ ! -x "$UV_EXE" || "$FORCE" == "1" ]]; then + if [[ "$GLOBAL" == "1" ]]; then + download "https://github.com/astral-sh/uv/releases/latest/download/$uv_file" "$UV_TGZ" + else + download "$VPS_BASE/uv/$uv_file" "$UV_TGZ" + fi + extract_tgz_clean "$UV_TGZ" "$UV_EXTRACT" + found_uv="$(find "$UV_EXTRACT" -type f -name uv | head -n 1 || true)" + [[ -n "$found_uv" ]] || die "uv not found in archive" + cp "$found_uv" "$UV_EXE" + chmod +x "$UV_EXE" +fi +ok "uv: $($UV_EXE --version)" + +export UV_PYTHON_INSTALL_DIR="$PORTABLE_ROOT/uv-python" +export UV_CACHE_DIR="$PORTABLE_ROOT/uv-cache" +if [[ "$GLOBAL" == "1" ]]; then + unset UV_DEFAULT_INDEX PIP_INDEX_URL +else + export UV_DEFAULT_INDEX="$MAINLAND_INDEX" + export PIP_INDEX_URL="$MAINLAND_INDEX" +fi +export PATH="$BIN:$PATH" + +say "Installing Python $PYTHON_VERSION via uv" +"$UV_EXE" python install "$PYTHON_VERSION" +PYTHON_EXE="$($UV_EXE python find "$PYTHON_VERSION")" +[[ -x "$PYTHON_EXE" ]] || die "uv installed Python but executable was not found" +ok "Python: $($PYTHON_EXE --version)" +PYTHON_DIR="$(dirname "$PYTHON_EXE")" +export PATH="$BIN:$PYTHON_DIR:$PATH" + +# git: macOS/Linux use system git. In mainland mode it is not required for source fetch. +GIT_EXE="" +if command -v git >/dev/null 2>&1; then + GIT_EXE="$(command -v git)" + ok "git: $($GIT_EXE --version)" +elif [[ "$GLOBAL" == "1" ]]; then + die "GLOBAL=1 requires git. Install git with your system package manager, then rerun." +else + say "git not found; continuing because mainland mode uses VPS zip. Install git later if needed." +fi + +# Fetch/update GenericAgent source. +if [[ "$GLOBAL" == "1" ]]; then + say "Cloning GenericAgent from GitHub" + if [[ -n "$(find "$GA_DIR" -mindepth 1 -maxdepth 1 ! -name .portable -print -quit)" ]]; then + [[ "$FORCE" == "1" ]] || die "Install dir contains files. Re-run with FORCE=1 to replace source while preserving portable tools." + remove_source_files + fi + TMP_CLONE="$CACHE/ga-clone" + rm -rf "$TMP_CLONE" + "$GIT_EXE" clone --depth 1 "$REPO_URL" "$TMP_CLONE" + copy_contents "$TMP_CLONE" "$GA_DIR" + rm -rf "$TMP_CLONE" +else + say "Downloading GenericAgent package from VPS" + download "$GA_ZIP_URL" "$GA_ZIP" + extract_zip_clean "$GA_ZIP" "$GA_EXTRACT" + SRC_DIR="$GA_EXTRACT/GenericAgent" + [[ -d "$SRC_DIR" ]] || SRC_DIR="$GA_EXTRACT" + remove_source_files + copy_contents "$SRC_DIR" "$GA_DIR" +fi +ok "GenericAgent source ready: $GA_DIR" + +# Install basic dependencies and project in editable mode into portable Python. +say "Installing GenericAgent dependencies via uv pip" +install_args=(pip install --python "$PYTHON_EXE" --break-system-packages) +if [[ "$GLOBAL" != "1" ]]; then install_args+=(--index-url "$MAINLAND_INDEX"); fi +install_args+=("${DEPS[@]}") +"$UV_EXE" "${install_args[@]}" + +if [[ -f "$GA_DIR/pyproject.toml" ]]; then + project_args=(pip install --python "$PYTHON_EXE" --break-system-packages) + if [[ "$GLOBAL" != "1" ]]; then project_args+=(--index-url "$MAINLAND_INDEX"); fi + project_args+=(-e "$GA_DIR") + "$UV_EXE" "${project_args[@]}" +fi + +# Try-install pywebview (optional UI). Failure is non-fatal. +say "Attempting to install pywebview (optional, failure is OK)" +webview_args=(pip install --python "$PYTHON_EXE" --break-system-packages) +if [[ "$GLOBAL" != "1" ]]; then webview_args+=(--index-url "$MAINLAND_INDEX"); fi +webview_args+=("pywebview>=4.0") +if "$UV_EXE" "${webview_args[@]}" 2>/dev/null; then + ok "pywebview installed successfully" +else + printf '\033[33m[warn]\033[0m pywebview install failed. This is optional.\n' + printf ' On Linux, pywebview requires system GTK/WebKit libraries.\n' + printf ' Install them first, e.g.:\n' + printf ' Debian/Ubuntu: sudo apt install python3-gi gir1.2-webkit2-4.1 libgirepository1.0-dev\n' + printf ' Fedora: sudo dnf install python3-gobject webkit2gtk4.1\n' + printf ' macOS: usually works out of the box (uses PyObjC)\n' + printf ' Then retry: uv pip install pywebview\n' +fi + +# Activation script: portable paths are intentionally before system PATH. +if [[ "$GLOBAL" == "1" ]]; then + cat > "$ENV_SH" < "$ENV_SH" < mykey.py" + fi +fi + +# Final banner +echo "" +if [[ "$GLOBAL" == "1" ]]; then + cat <
    " + html.escape("\n".join(lines)) + "
    " + +class _H(BaseHTTPRequestHandler): + def do_GET(self): + b = _page().encode("utf-8"); self.send_response(200); self.send_header("Content-Type", "text/html; charset=utf-8"); self.end_headers(); self.wfile.write(b) + def do_POST(self): + global _TASK_SLUG, _PLANNED + if self.path != "/exec": self.send_response(404); self.end_headers(); return + n = int(self.headers.get("Content-Length", "0")); req = json.loads(self.rfile.read(n).decode("utf-8")) + out = io.StringIO(); err = io.StringIO(); rc = 0 + with _exec_lock, redirect_stdout(out), redirect_stderr(err): + _bind(req["rundir"]); _note("exec: " + req.get("path", " + + \ No newline at end of file diff --git a/assets/tmwd_cdp_bridge/popup.js b/assets/tmwd_cdp_bridge/popup.js new file mode 100644 index 000000000..730ed21a9 --- /dev/null +++ b/assets/tmwd_cdp_bridge/popup.js @@ -0,0 +1,24 @@ +document.addEventListener('DOMContentLoaded', () => { + const out = document.getElementById('out'); + const btn = document.getElementById('refresh'); + btn.addEventListener('click', fetchCookies); + fetchCookies(); +}); + +async function fetchCookies() { + const out = document.getElementById('out'); + try { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (!tab?.url) { out.textContent = 'No active tab'; return; } + const resp = await chrome.runtime.sendMessage({ cmd: 'cookies', url: tab.url }); + if (!resp?.ok) { out.textContent = 'Error: ' + (resp?.error || 'unknown'); return; } + if (!resp.data.length) { out.textContent = '(no cookies)'; return; } + // 展示带标记 + out.textContent = resp.data.map(c => + `${c.name}=${c.value}` + (c.httpOnly ? ' [H]' : '') + (c.secure ? ' [S]' : '') + (c.partitionKey ? ' [P]' : '') + ).join('\n'); + // 自动复制 name=value; 格式到剪贴板 + const str = resp.data.map(c => `${c.name}=${c.value}`).join('; '); + await navigator.clipboard.writeText(str); + } catch (e) { out.textContent = 'Error: ' + e.message; } +} \ No newline at end of file diff --git a/assets/tools_schema.json b/assets/tools_schema.json index f121ec228..27b00a382 100644 --- a/assets/tools_schema.json +++ b/assets/tools_schema.json @@ -1,68 +1,73 @@ [ {"type": "function", "function": { "name": "code_run", - "description": "代码执行器。优先使用python,仅在必要系统操作时使用 powershell。注意:执行的代码必须放在在回复正文中,以 ```python 或 ```powershell 代码块的形式。严禁在代码中硬编码大量数据,如有需要应通过文件读取。", + "description": "Code executor. Prefer python. Multi-call OK, use script param. Reply code block is executed if no script arg; prefer for single call to avoid escaping. No hardcoding bulk data", "parameters": {"type": "object", "properties": { - "type": {"type": "string", "enum": ["python", "powershell"], "description": "执行环境类型,默认为 python。", "default": "python"}, - "timeout": {"type": "integer", "description": "执行超时时间(秒),默认 60。", "default": 60}, - "cwd": {"type": "string", "description": "工作目录,默认为当前工作目录。"}}} + "script": {"type": "string", "description": "[Mutually exclusive] NEVER use this param when use reply code block."}, + "type": {"type": "string", "enum": ["python", "powershell"], "description": "Code type", "default": "python"}, + "timeout": {"type": "integer", "description": "in seconds", "default": 60}, + "cwd": {"type": "string", "description": "Working directory, defaults to cwd"}, + "inline_eval": {"type": "boolean", "description": "DO NOT USE except explicitly specified."}}} }}, {"type": "function", "function": { "name": "file_read", - "description": "读取文件内容。建议在修改文件前先读取,以确保获取最新的上下文和行号。支持分页读取或关键字搜索。", + "description": "Read file. Read before modify for latest context and line numbers", "parameters": {"type": "object", "properties": { - "path": {"type": "string", "description": "文件相对或绝对路径。"}, - "start": {"type": "integer", "description": "起始行号(从 1 开始)。", "default": 1}, - "count": {"type": "integer", "description": "读取的行数。", "default": 200}, - "keyword": {"type": "string", "description": "可选搜索关键字。如果提供,将返回第一个匹配项(忽略大小写)及其周边的内容。"}, - "show_linenos": {"type": "boolean", "description": "是否显示行号,建议开启以辅助 file_patch 定位。", "default": true}}, "required": ["path"]} + "path": {"type": "string", "description": "Relative or absolute"}, + "start": {"type": "integer", "description": "Start line number (1-based)"}, + "count": {"type": "integer", "description": "Number of lines to read", "default": 200}, + "show_linenos": {"type": "boolean", "description": "Show line numbers", "default": true}}} }}, {"type": "function", "function": { "name": "file_patch", - "description": "精细化局部文件修改。在文件中寻找唯一的 old_content 块并替换为 new_content。要求 old_content 必须在文件中唯一存在,且空格、缩进、换行必须与原文件完全一致。如果匹配失败,请使用 file_read 重新确认文件内容。", + "description": "Replace unique old_content with new_content. Exact match required (whitespace/indentation). On failure, file_read to recheck", "parameters": {"type": "object", "properties": { - "path": {"type": "string", "description": "文件路径。"}, - "old_content": {"type": "string", "description": "文件中需要被替换的原始文本块(需确保唯一性)。"}, - "new_content": {"type": "string", "description": "替换后的新文本内容。"}}, "required": ["path", "old_content", "new_content"]} + "path": {"type": "string", "description": "File path"}, + "old_content": {"type": "string", "description": "Original text block to replace (must be unique)"}, + "new_content": {"type": "string", "description": "New content. Supports {{file:path:startLine:endLine}} to ref file lines, auto-expanded"}}} }}, {"type": "function", "function": { "name": "file_write", - "description": "用于文件的新建、全量覆盖或追加写入。对于精细的代码修改,应优先使用 file_patch。注意:要写入的内容必须放在回复正文的 标签或代码块中。", + "description": "Create/overwrite/append files. HUGE edits ONLY. Supports {{file:path:startLine:endLine}}, auto-expanded", "parameters": {"type": "object", "properties": { - "path": {"type": "string", "description": "文件路径。"}, - "mode": {"type": "string", "enum": ["overwrite", "append", "prepend"], "description": "写入模式覆盖、追加或在开头追加。", "default": "overwrite"}}, "required": ["path"]} + "path": {"type": "string", "description": "File path"}, + "content": {"type": "string"}, + "mode": {"type": "string", "enum": ["overwrite", "append", "prepend"], "description": "Write mode", "default": "overwrite"}}} }}, {"type": "function", "function": { "name": "web_scan", - "description": "获取当前页面的简化HTML内容和标签页列表。注意:简化会过滤边栏、浮动元素等非主体内容,如需查看被过滤内容请用execute_js。切换页面后一般应先调用查看。", + "description": "Get simplified HTML and tab list. Removes hidden/floating/covered elements. Call after switching pages", "parameters": {"type": "object", "properties": { - "tabs_only": {"type": "boolean", "description": "仅返回标签页列表和当前标签信息,不获取HTML内容。", "default": false}, - "switch_tab_id": {"type": "string", "description": "可选的标签页 ID。如果提供,系统将在扫描前切换到该标签页。"}}} + "tabs_only": {"type": "boolean", "description": "Show tab list only, no HTML"}, + "switch_tab_id": {"type": "string", "description": "[Optional] Tab ID to switch to"}, + "text_only": {"type": "boolean", "description": "Plain text only, no HTML"}}} }}, {"type": "function", "function": { "name": "web_execute_js", - "description": "万能网页操控工具。通过执行 JavaScript 脚本实现对浏览器的完全控制(如点击、滚动、提取特定数据)。鼓励在有把握情况下(记忆中有selector/做法等)精准使用以减少web_scan调用。执行结果可选择保存到本地文件进行后续分析。", + "description": "Execute JS. Multi-call OK with different switch_tab_id. No guessing. Act accurately to reduce web_scan calls. Execute JS in ```javascript blocks if no script arg, prefer to avoid escaping", "parameters": {"type": "object", "properties": { - "script": {"type": "string", "description": "要执行的 JavaScript 代码。"}, - "save_to_file": {"type": "string", "description": "可选。将 JS 执行结果(js_return)保存到的文件路径。该功能不支持 await 等异步结果。"}}, "required": ["script"]} + "script": {"type": "string", "description": "[Mutually exclusive] JS code or script path. NEVER use this param when use reply code block"}, + "save_to_file": {"type": "string", "description": "file path; **only** for long result"}, + "no_monitor": {"type": "boolean", "description": "Skip page change monitoring, saves 2-3s. Only for reads, not for page actions"}, + "switch_tab_id": {"type": "string", "description": "[Optional] Tab ID to switch to before executing"}}} }}, {"type": "function", "function": { "name": "update_working_checkpoint", - "description": "短期工作便签,内容每轮自动注入,防止长任务中关键信息丢失。要在任务前中期而非结束时调用,新任务切换时应当及时使用清除之前影响。何时调用:(1)即将切换子任务、上下文将被大量新信息冲刷前,存入当前路径/参数/进度;(2)获得后续步骤必需的关键发现后;(3)分步任务完成一步后更新为本步结果+下一步要求。原则:只存N轮后可能忘记但后面还要用的信息,刚发生的不用存。宁可多更新不可丢关键上下文。", + "description": "Short-term working notepad, auto-injected each turn to prevent info loss in long tasks. Call during early/mid stages, not at end. When: (1) after reading SOP, store user needs & key constraints (skip for simple 1-2 step tasks); (2) before subtask switch or context flush; (3) after repeated failures, re-read SOP and must store new findings; (4) on new task, update content, clear old progress but keep valid constraints.\n\nDon't call: simple tasks (1-2 steps), task completed (use long-term memory tool)", "parameters": {"type": "object", "properties": { - "key_info": {"type": "string", "description": "替换当前便签(<200 tokens)。只写后续必须记住的:文件路径、关键参数/发现、当前进度、下一步计划、要避的坑。刚完成的和上下文中显而易见的不写,省空间给真正容易丢的信息。"}, - "related_sop": {"type": "string", "description": "相关sop名称,可以多个,必要时需要再读"}}} + "key_info": {"type": "string", "description": "Replaces current notepad (<200 tokens). Incremental update: review existing, keep valid, add/remove/modify. Store: pitfalls, user requirements, key params/findings, file paths, progress, next steps. Don't store: ephemeral info, obvious context, old task info when user switched tasks. Prefer over-updating over losing key info"}, + "related_sop": {"type": "string", "description": "Related SOP names, tips for further re-read"}}} }}, {"type": "function", "function": { "name": "ask_user", - "description": "当需要用户决策、提供额外信息或遇到无法自动解决的阻碍时,调用此工具中断任务并提问。", + "description": "Interrupt task to ask user when needing decisions, extra info, or facing unresolvable blockers. Multiple questions should in one call", "parameters": {"type": "object", "properties": { - "question": {"type": "string", "description": "向用户提出的明确问题。"}, - "candidates": {"type": "array", "items": {"type": "string"}, "description": "提供给用户的可选快捷选项列表。"}}, "required": ["question"]} + "question": {"type": "string", "description": "For multiple questions, one per line (contain candidates)"}, + "candidates": {"type": "array", "items": {"type": "string"}, "description": "Optional quick-select choices for a single question only. Leave empty for multi-questions"}}} }}, {"type": "function", "function": { "name": "start_long_term_update", - "description": "准备开始提炼记忆。发现值得长期记忆的信息(环境事实/用户偏好/避坑经验)时调用此工具。一次用户对话只允许调用一次,已记忆更新或在自主流程内时无需调用。", + "description": "Start distilling long-term memory. Call when discovering info worth remembering (env facts/user prefs/lessons learned). Skip if memory already updated or in autonomous flow. Must call when a task that took 15+ turns is completed", "parameters": {"type": "object", "properties": {}}} } ] \ No newline at end of file diff --git a/assets/tools_schema_cn.json b/assets/tools_schema_cn.json new file mode 100644 index 000000000..98fbc211b --- /dev/null +++ b/assets/tools_schema_cn.json @@ -0,0 +1,73 @@ +[ + {"type": "function", "function": { + "name": "code_run", + "description": "代码执行器。优先使用python。支持Multi-call,并行时用script参数。无script参数时正文代码块会被执行,单次调用优先使用以免转义。禁硬编码大量数据", + "parameters": {"type": "object", "properties": { + "script": {"type": "string", "description": "[Optional] 要执行的代码。为免转义建议留空,改用正文代码块(与此参数互斥)"}, + "type": {"type": "string", "enum": ["python", "powershell"], "description": "代码类型", "default": "python"}, + "timeout": {"type": "integer", "description": "执行超时时间(秒)", "default": 60}, + "cwd": {"type": "string", "description": "工作目录,默认为当前工作目录"}, + "inline_eval": {"type": "boolean", "description": "不允许使用除非明确要求"}}} + }}, + {"type": "function", "function": { + "name": "file_read", + "description": "读取文件内容。建议在修改文件前先读取,以确保获取最新的上下文和行号。支持分页读取或关键字搜索", + "parameters": {"type": "object", "properties": { + "path": {"type": "string", "description": "文件相对或绝对路径"}, + "start": {"type": "integer", "description": "起始行号(从 1 开始)"}, + "count": {"type": "integer", "description": "读取的行数", "default": 200}, + "show_linenos": {"type": "boolean", "description": "是否显示行号,建议开启以辅助 file_patch 定位", "default": true}}} + }}, + {"type": "function", "function": { + "name": "file_patch", + "description": "精细化局部文件修改。在文件中寻找唯一的 old_content 块并替换为 new_content。要求 old_content 必须在文件中唯一存在,且空格、缩进、换行必须与原文件完全一致。如果匹配失败,请使用 file_read 重新确认文件内容", + "parameters": {"type": "object", "properties": { + "path": {"type": "string", "description": "文件路径"}, + "old_content": {"type": "string", "description": "文件中需要被替换的原始文本块(需确保唯一性)"}, + "new_content": {"type": "string", "description": "替换后的新文本内容。支持 {{file:路径:起始行:结束行}} 语法引用文件内容,写入前自动展开"}}} + }}, + {"type": "function", "function": { + "name": "file_write", + "description": "用于文件的新建、全量覆盖或追加写入。对于精细的代码修改,应优先使用 file_patch。写入内容支持 {{file:路径:起始行:结束行}} 语法引用文件片段,写入前自动展开", + "parameters": {"type": "object", "properties": { + "path": {"type": "string", "description": "文件路径"}, + "content": {"type": "string"}, + "mode": {"type": "string", "enum": ["overwrite", "append", "prepend"], "description": "写入模式覆盖、追加或在开头追加", "default": "overwrite"}}} + }}, + {"type": "function", "function": { + "name": "web_scan", + "description": "获取当前页面的简化HTML内容和标签页列表。会移除隐藏/浮动/被遮盖的元素。切换页面后一般应先调用查看", + "parameters": {"type": "object", "properties": { + "tabs_only": {"type": "boolean", "description": "仅返回标签页列表和当前标签信息,不获取HTML内容"}, + "switch_tab_id": {"type": "string", "description": "可选的标签页 ID。如果提供,系统将在扫描前切换到该标签页"}, + "text_only": {"type": "boolean", "description": "只要纯文本不要HTML"}}} + }}, + {"type": "function", "function": { + "name": "web_execute_js", + "description": "执行JS。支持Multi-call,用不同switch_tab_id并行操作多标签页。禁止猜测,准确操作以减少 web_scan 调用。无script参数时执行正文 ```javascript 块,以免转义", + "parameters": {"type": "object", "properties": { + "script": {"type": "string", "description": "[Optional] JS代码或路径。为免转义建议留空,改用正文代码块(与此参数互斥)"}, + "save_to_file": {"type": "string", "description": "结果存文件,适合返回值较长时"}, + "no_monitor": {"type": "boolean", "description": "跳过页面变更监控,省2-3秒。仅在纯读取信息时设置,页面操作时不要设置"}, + "switch_tab_id": {"type": "string", "description": "可选的标签页 ID,切换到该标签页执行"}}} + }}, + {"type": "function", "function": { + "name": "update_working_checkpoint", + "description": "短期工作便签,每轮自动注入上下文,防长任务信息丢失。前中期调用,非结束时。何时调用:(1)任务开始读SOP后,存用户需求和关键约束/参数(简单1-2步任务除外);(2)子任务切换或上下文即将被冲刷前;(3)多次重试失败后,重读SOP并必须调用存储新发现;(4)切换新任务时更新内容,清旧进度但保留仍有效的约束。\n\n何时不调用:简单任务(1-2步且无严重约束)、任务已完成时(应当用长期结算工具)", + "parameters": {"type": "object", "properties": { + "key_info": {"type": "string", "description": "替换当前便签(<200 tokens)。增量更新:先回顾现有内容,保留仍有效的,再增删改。存:要避的坑、用户原始需求、关键参数/发现、文件路径、当前进度、下一步计划。不存:马上要用用完即丢的、上下文中显而易见的、用户已换全新任务时的旧任务信息。宁多更新不丢关键"}, + "related_sop": {"type": "string", "description": "相关sop名称,可以多个,必要时需要再读"}}} + }}, + {"type": "function", "function": { + "name": "ask_user", + "description": "当需要用户决策、提供额外信息或遇到无法自动解决的阻碍时,调用此工具中断任务并提问。多题时应一次调用", + "parameters": {"type": "object", "properties": { + "question": {"type": "string", "description": "多题时每题*独占*一行(带选项)"}, + "candidates": {"type": "array", "items": {"type": "string"}, "description": "仅单题时可用。多题时留空"}}} + }}, + {"type": "function", "function": { + "name": "start_long_term_update", + "description": "准备开始提炼记忆。发现值得长期记忆的信息(环境事实/用户偏好/避坑经验)时调用此工具。已记忆更新或在自主流程内时无需调用。超15轮完成的任务必须调用以沉淀经验", + "parameters": {"type": "object", "properties": {}}} + } +] \ No newline at end of file diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 000000000..eec10a585 --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,298 @@ +# 🚀 新手上手指南 + +> 完全没接触过编程也没关系,跟着做就行。Mac / Windows 都适用。 +> +> 如果你已经有 Python 环境,直接跳到[第 2 步](#2-配置-api-key)。 + +--- + +## 1. 安装 Python + +### Mac + +打开「终端」(启动台搜索 "终端" 或 "Terminal"),粘贴这行命令然后回车: + +```bash +brew install python +``` + +如果提示 `brew: command not found`,说明还没装 Homebrew,先粘贴这行: + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +装完后再执行 `brew install python`。 + +### Windows + +1. 打开 [python.org/downloads](https://www.python.org/downloads/),点黄色大按钮下载 +2. 运行安装包,**底部的 "Add Python to PATH" 一定要勾上** +3. 点 "Install Now" + +### 验证 + +终端 / 命令提示符里输入: + +```bash +python3 --version +``` + +看到 `Python 3.x.x` 就 OK。Windows 上也可以试 `python --version`。 + +> ⚠️ **版本提示**:推荐 **Python 3.11 或 3.12**。不要使用 3.14(与 pywebview 等依赖不兼容)。 + +--- + +## 2. 配置 API Key + +### 下载项目 + +最方便的方式是 **一键安装**(自带隔离 Python 环境 + Git + 桌面端): + +**Windows PowerShell** + +```powershell +powershell -ExecutionPolicy Bypass -c "irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" +``` + +**Linux / macOS** + +```bash +curl -fsSL http://fudankw.cn:9000/files/ga_install.sh | bash +``` + +或者手动 clone(开发者): + +```bash +git clone https://github.com/lsdefine/GenericAgent.git +cd GenericAgent +uv venv && uv pip install -e ".[ui]" +``` + +也可以走最朴素的 ZIP:[GitHub 仓库页面](https://github.com/lsdefine/GenericAgent) → 点绿色 **Code** → **Download ZIP** → 解压到喜欢的位置。 + +> 💡 **让 Claude / Codex 等 Agent 帮你装**:把下面这条 curl 丢给它,它会按官方指南替你完成安装: +> ```bash +> curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/refs/heads/main/docs/installation_zh.md +> ``` +> +> 📖 平台差异、排障、升级流程见 [`docs/installation_zh.md`](installation_zh.md)。 + +### 创建配置文件 + +进入项目文件夹,把 `mykey_template.py` 复制一份,重命名为 `mykey.py`。 + +用任意文本编辑器打开 `mykey.py`,填入你的 API 信息。**选一种填就行**,不用的配置删掉或留着不管都行。 + +> 💡 也可以运行交互式向导 `python assets/configure_mykey.py`,按提示选择厂商、填入 Key 即可自动生成 `mykey.py`。 + +### 配置示例 + +**推荐首选:Claude 原生协议**: + +```python +# 变量名同时含 'native' 和 'claude' → NativeClaudeSession(API 原生工具字段) +native_claude_config = { + 'name': 'claude', # /llms 显示名 & mixin 引用名 + 'apikey': 'sk-xxx', # sk-ant- 走 x-api-key;其它走 Bearer + 'apibase': 'https://api.anthropic.com', # 官方直连;反代渠道填对应地址 + 'model': 'claude-opus-4-7', # [1m] 后缀触发 1M 上下文 beta + # 'fake_cc_system_prompt': True, # CC switch / 反代渠道必须置 True +} +``` + +**也支持:OpenAI 原生协议**: + +```python +# 变量名同时含 'native' 和 'oai' → NativeOAISession +native_oai_config = { + 'name': 'gpt', # /llms 显示名 & mixin 引用名 + 'apikey': 'sk-xxx', + 'apibase': 'https://api.openai.com/v1', # 自动补 /v1/chat/completions + 'model': 'gpt-5.5', +} +``` + +**进阶:Mixin 故障转移**(多 session 自动切换,最稳的玩法): + +```python +# llm_nos 按优先级排列;首项失败按指数退避切下一项 +mixin_config = { + 'llm_nos': ['claude', 'gpt'], # 与上面 native_* 的 name 字段对应 + 'max_retries': 10, + 'base_delay': 0.5, +} +``` + +> 💡 完整字段说明(`thinking_type` / `reasoning_effort` / `context_win` / `proxy` / Zhipu / MiniMax / Kimi / OpenRouter 等渠道示例)见 `mykey_template.py` 顶部注释。 + +### 关键规则 + +**变量命名决定 Session 类型**(不是模型名决定的): + +| 变量名包含 | 触发的 Session | 工具协议 | 适用场景 | +|-----------|---------------|---------|---------| +| `native` + `claude` | NativeClaudeSession | API 原生 tool 字段 | **推荐首选** — Claude 原生协议 | +| `native` + `oai` | NativeOAISession | API 原生 tool 字段 | GPT/o 系列、OAI 兼容渠道 | +| `mixin` | MixinSession | 多 session 故障转移 | 最稳;要求被引用 session 全为 native | +| `claude`(不含 `native`) | ClaudeSession | 文本协议工具 | **deprecated**,后续版本可能移除 | +| `oai`(不含 `native`) | LLMSession | 文本协议工具 | **deprecated**,后续版本可能移除 | + +**`apibase` 填写规则**(会自动拼接端点路径): + +| 你填的内容 | 系统行为 | +|-----------|---------| +| `http://host:2001` | 自动补 `/v1/chat/completions` | +| `http://host:2001/v1` | 自动补 `/chat/completions` | +| `http://host:2001/v1/chat/completions` | 直接使用,不拼接 | + +--- + +## 3. 初次启动 + +终端里进入项目文件夹,运行: + +```bash +cd 你的解压路径 +python3 agentmain.py +``` + +这就是**命令行模式**,已经可以用了。你会看到一个输入提示符,直接打字发送任务即可。 + +试试你的第一个任务: + +``` +帮我在桌面创建一个 hello.txt,内容是 Hello World +``` + +> 💡 Windows 上如果 `python3` 不识别,换成 `python agentmain.py`。 + +--- + +## 4. 让 Agent 自己装依赖 + +Agent 启动后,只需要一句话,它就会自己搞定所有依赖: + +``` +请查看你的代码,安装所有用得上的 python 依赖 +``` + +Agent 会自己读代码、找出需要的包、全部装好。 + +> ⚠️ 如果遇到网络问题导致 Agent 无法调用 API,可能需要先手动装一个包: +> ```bash +> pip install requests +> ``` + +### 升级到图形界面 + +依赖装完后,可以选择适合你的前端: + +| 前端 | 启动命令 | 说明 | +|------|---------|------| +| **桌面端** | 双击 `frontends/GenericAgent.exe`(Windows 一键安装自带) | 真原生窗口,零终端依赖 | +| **TUI v3** | `python frontends/tui_v3.py` | 基于块的滚屏回看、resize 重排、每终端独立配色,跨终端体验一致 | +| **TUI v2** | `python frontends/tuiapp_v2.py` | Textual 键盘驱动界面,图片粘贴折叠、`/llm`/`/export`/`/continue` 选择器 | +| **Streamlit / 悬浮窗** | `python launch.pyw` | 浏览器中打开的 Streamlit UI,附带桌面悬浮窗 | + +> 💡 Windows 下推荐用 **Git Bash** 跑 TUI;PowerShell / cmd 对 Unicode 和键位支持较弱。仍异常时请直接告诉 Agent:「参考 Claude Code 在 Windows 终端的最佳配置帮我把 TUI 修一遍」。 + +### 可选:让 Agent 帮你做的事 + +``` +请帮我建立 git 连接,方便以后更新代码 +``` + +Agent 会自动配好。如果你电脑上没有 Git,它也会帮你下载 portable 版。 + +``` +请帮我在桌面创建一个 launch.pyw 的快捷方式 +``` + +这样以后双击桌面图标就能启动,不用再开终端了。 + +--- + +## 5. 能力解锁 + +环境跑起来之后,你可以逐步解锁更多能力。每一项都只需要**对 Agent 说一句话**: + +### 基础能力 + +| 能力 | 对 Agent 说 | 说明 | +|------|-----------|------| +| **PowerShell 脚本执行** | `帮我解锁当前用户的 PowerShell ps1 执行权限` | Windows 默认禁止运行 .ps1 脚本 | +| **全局文件搜索** | `安装并配置 Everything 命令行工具进 PATH` | 毫秒级全盘文件搜索 | + +### 浏览器自动化 + +| 能力 | 对 Agent 说 | 说明 | +|------|-----------|------| +| **Web 工具解锁** | `执行 web setup sop,解锁 web 工具` | 注入浏览器插件,使 Agent 能直接操控网页 | + +解锁后,Agent 可以在**保留你登录态**的真实浏览器中操作: + +``` +打开淘宝,搜索 iPhone 16,按价格排序 +去 B 站,查看我最近看过的历史视频 +``` + +### 进阶能力 + +| 能力 | 对 Agent 说 | 说明 | +|------|-----------|------| +| **OCR** | `用rapidocr配置你的ocr能力并存入记忆` | 让 Agent 能"看到"屏幕文字 | +| **屏幕视觉** | `仿造你的llmcore,写个调用vision的能力并存入记忆` | 让 Agent 能"看到"屏幕内容 | +| **移动端控制** | `配置 ADB 环境,准备连接安卓设备` | 通过 USB/WiFi 控制 Android 手机 | + +### 聊天平台接入(可选) + +接入后可以随时随地通过手机给电脑上的 Agent 发指令。 + +对 Agent 说:`看你的代码,帮我配置 XX 平台的机器人接入` + +支持的平台:**微信个人Bot** / QQ / 飞书 / 企业微信 / 钉钉 / Telegram + +> Agent 会自动读取代码、引导你完成配置。 + +### 高级模式 + +以下模式全部**自文档化**——不用查手册,直接问 Agent 即可: + +| 模式 | 对 Agent 说 | +|------|------------| +| **Reflect(反射)** | `查看你的代码,告诉我你的 reflect 模式怎么启用` | +| **计划任务** | `查看你的代码,告诉我你的计划任务模式怎么启用` | +| **Plan(规划)** | `查看你的代码,告诉我你的 plan 模式怎么启用` | +| **SubAgent(子代理)** | `查看你的代码,告诉我你的 subagent 模式怎么启用` | +| **自主探索** | `查看你的代码,告诉我你的自主探索模式怎么启用` | +| **Goal** | `查看你的代码,告诉我 goal 模式怎么启用` | +| **Goal Hive(多 worker 协作)** | `查看你的代码,告诉我 goal hive 模式怎么启用` | +| **Conductor(多 subagent 编排)** | `查看你的代码,告诉我 conductor 模式怎么启用` | +| **Morphling(吞噬外部项目)** | `查看你的代码,告诉我 morphling 模式怎么启用` | + +> 💡 这就是 GenericAgent 的核心设计理念:**代码即文档**。Agent 能读懂自己的源码,所以任何功能你都可以直接问它。 + +--- + +## 💡 使用越久越强 + +GenericAgent 不预设技能,而是**靠使用进化**。每完成一个新任务,它会自动将执行路径固化为 Skill,下次遇到类似任务直接调用。 + +你不需要管理这些 Skill,Agent 会自动处理。使用时间越长,积累的技能越多,最终形成一棵完全属于你的专属技能树。 + +> 💡 如果你觉得某些重要信息 Agent 没有记住,可以直接告诉它:`把这个记到你的记忆里`,它会主动记忆。 + +**其他 Claw 的 Skill 也可以直接复用:** + +- 让 Agent 搜索:`帮我找个做 XXX 的 skill` → 完成后 → `加入你的记忆中` +- 直接指定来源:`访问 XXX 文件夹/URL,按照这个 skill 做 XXX` + +**保持更新:** + +对 Agent 说:`git 更新你的代码,然后看看 commit 有什么新功能` + +> Agent 会自动 pull 最新代码并解读 commit log,告诉你新增了什么能力。 + +> 更多细节请参阅 [README.md](../README.md) 或 [详细版图文教程](https://my.feishu.cn/wiki/CGrDw0T76iNFuskmwxdcWrpinPb)。 diff --git a/docs/SETUP_FEISHU.md b/docs/SETUP_FEISHU.md new file mode 100644 index 000000000..da2ca05a3 --- /dev/null +++ b/docs/SETUP_FEISHU.md @@ -0,0 +1,301 @@ +# 飞书 Agent 配置指南 + +> 让你的个人电脑变成飞书机器人的大脑,随时随地通过飞书对话控制你的电脑。 + +--- + +## 📋 目录 + +1. [前置条件](#前置条件) +2. [方案选择](#方案选择) +3. [企业用户配置](#企业用户配置) +4. [个人用户配置](#个人用户配置) +5. [项目配置](#项目配置) +6. [运行与测试](#运行与测试) +7. [常见问题](#常见问题) + +--- + +## 前置条件 + +### 必需环境 + +- Python 3.8+ +- 本项目完整代码 +- LLM API 密钥(Claude/OpenAI 等,已在 `llmcore/mykeys` 中配置) + +### 安装依赖 + +```bash +pip install lark-oapi +``` + +--- + +## 方案选择 + +| 你的情况 | 推荐方案 | 预计耗时 | +| ------------------ | -------------------------- | --------- | +| 公司已有飞书企业版 | [企业用户配置](#企业用户配置) | 5-10分钟 | +| 个人用户/学习测试 | [个人用户配置](#个人用户配置) | 10-15分钟 | + +--- + +## 企业用户配置 + +> 适用于:你的公司使用飞书,你有权限创建应用或联系管理员审批 + +### 步骤 1:创建应用 + +1. 访问 [飞书开放平台](https://open.feishu.cn/) +2. 登录你的企业飞书账号 +3. 点击右上角「创建应用」→「企业自建应用」 +4. 填写应用信息: + - 应用名称:`我的Agent助手`(可自定义) + - 应用描述:`个人AI助手` + - 应用图标:可选 + +### 步骤 2:添加机器人能力 + +1. 进入应用详情页 +2. 左侧菜单选择「添加应用能力」 +3. 找到「机器人」,点击「添加」 +4. 配置机器人信息(可保持默认) + +### 步骤 3:配置权限 + +1. 左侧菜单「权限管理」→「API 权限」 +2. 搜索并开通以下权限: + - `im:message` - 获取与发送单聊、群组消息 + - `im:message:send_as_bot` - 以应用身份发送消息 + - `contact:user.id:readonly` - 获取用户 ID + +### 步骤 4:获取凭证 + +1. 左侧菜单「凭证与基础信息」 +2. 记录以下信息: + - **App ID**:`cli_xxxxxxxx` + - **App Secret**:`xxxxxxxxxxxxxxxx` + +### 步骤 5:发布应用 + +1. 左侧菜单「版本管理与发布」 +2. 点击「创建版本」 +3. 填写版本信息,提交审核 +4. **联系企业管理员审批**(或自己是管理员直接审批) + +### 步骤 6:获取你的 Open ID + +1. 应用审批通过后,在飞书中搜索你的机器人 +2. 给机器人发送任意消息 +3. 运行以下代码获取你的 Open ID: + +```python +# 临时运行一次,获取 open_id +import lark_oapi as lark +from lark_oapi.api.im.v1 import * + +client = lark.Client.builder().app_id("你的APP_ID").app_secret("你的APP_SECRET").build() + +# 监听消息,打印发送者的 open_id +def handle(data): + print(f"你的 Open ID: {data.event.sender.sender_id.open_id}") + +# ... 或者查看 frontends/fsapp.py 运行时的日志输出 +``` + +--- + +## 个人用户配置 + +> 适用于:没有企业飞书账号,想个人测试使用 + +### 步骤 1:创建测试企业 + +1. 访问 [飞书开放平台](https://open.feishu.cn/) +2. 使用个人手机号注册/登录 +3. 点击右上角头像 →「创建测试企业」 +4. 填写企业名称(如:`我的测试工作区`) +5. 创建完成后,你就是这个测试企业的**管理员** + +### 步骤 2:创建应用 + +> 与企业用户步骤相同 + +1. 点击「创建应用」→「企业自建应用」 +2. 填写应用信息 + +### 步骤 3:添加机器人能力 + +1. 进入应用详情页 +2. 「添加应用能力」→「机器人」→「添加」 + +### 步骤 4:配置权限 + +1. 「权限管理」→「API 权限」 +2. 开通权限: + - `im:message` + - `im:message:send_as_bot` + - `contact:user.id:readonly` + +### 步骤 5:获取凭证 + +1. 「凭证与基础信息」 +2. 复制 **App ID** 和 **App Secret** + +### 步骤 6:发布应用(测试企业可自审批) + +1. 「版本管理与发布」→「创建版本」 +2. 提交后,进入 [飞书管理后台](https://feishu.cn/admin) +3. 「工作台」→「应用审核」→ 通过你的应用 + +### 步骤 7:在飞书客户端使用 + +1. 下载 [飞书客户端](https://www.feishu.cn/download) +2. 登录你的测试企业账号 +3. 搜索你创建的机器人名称 +4. 开始对话! + +--- + +## 项目配置 + +### 配置飞书凭证 + +编辑项目根目录的 `mykey.py`,添加: + +```python +# 飞书应用凭证 +fs_app_id = "cli_xxxxxxxxxxxxxxxx" # 替换为你的 App ID +fs_app_secret = "xxxxxxxxxxxxxxxx" # 替换为你的 App Secret + +# 允许使用的用户 Open ID 列表(可选,留空则允许所有人) +fs_allowed_users = [ + "ou_xxxxxxxxxxxxxxxxxxxxxxxx", # 你的 Open ID +] +``` + +### 确认 LLM 配置 + +确保 `llmcore/mykeys` 中已配置 LLM API 密钥: + +```python +# 示例:Claude API +claude_config = { + 'apikey': 'sk-ant-xxxxx', + 'apibase': 'https://api.anthropic.com', + 'model': 'claude-sonnet-4-20250514' +} +``` + +--- + +## 运行与测试 + +### 启动服务 + +```bash +cd /path/to/pc-agent-loop +python frontends/fsapp.py +``` + +### 预期输出 + +``` +================================================== +飞书 Agent 已启动(长连接模式) +App ID: cli_xxxxxxxxxxxxxxxx +等待消息... +================================================== +``` + +### 测试对话 + +1. 打开飞书客户端 +2. 找到你的机器人 +3. 发送:`你好` +4. 等待回复(首次可能需要几秒) + +--- + +## 可用命令 + +在与机器人对话时,可以使用以下特殊命令: + +| 命令 | 说明 | +| ---- | ---- | +| `/new` | 开始新对话,清除当前上下文 | +| `/stop` | 中止当前正在执行的任务 | +| `/restore <关键词>` | 恢复之前的对话上下文(根据关键词搜索历史记录) | + +### 命令示例 + +``` +/new # 清空对话,重新开始 +/stop # 停止正在运行的任务 +/restore 昨天的任务 # 恢复包含"昨天的任务"关键词的历史对话 +``` + +### 消息显示说明 + +- ⏳ 表示任务正在执行中 +- 消息会实时更新,无需等待完成 +- 超长回复会自动分段发送 + +--- + +## 常见问题 + +### Q: 提示「应用未发布」或「无权限」 + +**A:** 确保应用已发布且管理员已审批。测试企业用户需要在管理后台手动审批。 + +### Q: 发送消息后没有回复 + +**A:** 检查: + +1. `frontends/fsapp.py` 是否在运行 +2. 终端是否有错误日志 +3. LLM API 密钥是否配置正确 + +### Q: 提示「invalid app_id」 + +**A:** 检查 `mykey.py` 中的 `fs_app_id` 是否正确复制(包含 `cli_` 前缀) + +### Q: 如何获取自己的 Open ID? + +**A:** 运行 `frontends/fsapp.py` 后给机器人发消息,查看终端日志中的 `open_id` + +### Q: 能否多人同时使用? + +**A:** 不能。一个应用只能有一个长连接,连接到一台电脑。每个人需要创建自己的应用。 + +--- + +## 架构说明 + +``` +你的飞书 ←→ 飞书云 ←→ 长连接 ←→ frontends/fsapp.py ←→ Agent ←→ 你的电脑 + ↑ + 运行在你电脑上 +``` + +- 消息通过飞书云转发到你电脑上运行的 `frontends/fsapp.py` +- Agent 处理请求后,通过飞书 API 回复消息 +- **你的电脑必须保持运行** `frontends/fsapp.py` 才能响应消息 + +--- + +## 下一步 + +- 自定义 Agent 行为:编辑 `assets/sys_prompt.txt` +- 添加新工具:编辑 `assets/tools_schema.json` +- 查看日志:运行时观察终端输出 + +--- + +*文档版本:v1.1 | 更新日期:2026-03-07* + +**v1.1 更新内容:** +- 新增「可用命令」章节(/new, /stop, /restore) +- 新增消息显示说明(⏳ 进行中标记、实时更新等) diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 000000000..dab72278b --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,323 @@ +# Installation Guide + +This is the detailed installation guide for **GenericAgent**. + +Two audiences: + +- **[For Humans](#for-humans)** — you are installing GA for yourself. +- **[For LLM Agents](#for-llm-agents)** — you are a coding agent such as Claude Code, or Codex installing GA for a human user. Read that section first so you do not guess. + +> The shortest install commands live in the main [README](../README.md#-quick-start). This guide adds platform notes, key setup, verification, troubleshooting, and agent-safe rules. + +--- + +## For Humans + +### Prerequisites + +| Requirement | Notes | +|---|---| +| **OS** | Windows 10/11, macOS 12+, or a modern Linux distribution. | +| **Python** | Use **Python 3.11 or 3.12**. **Do not use Python 3.14** — it is incompatible with `pywebview` and a few GA dependencies. The one-line installer ships an isolated Python environment, so manual Python setup is usually unnecessary. | +| **Git** | Recommended for updates and self-evolution. | +| **LLM API key** | GA speaks two native protocols: **OpenAI-compatible** APIs and **Anthropic Claude native** APIs. GPT-family models, Claude, Kimi, MiniMax, DeepSeek, GLM, Qwen, Gemini through OAI-compatible gateways, and similar providers can be configured through `mykey.py`. | + +### Method 1: One-line install (recommended) + +This is the easiest path. It prepares an isolated runtime, downloads GenericAgent, installs the core dependencies, and gives you a ready-to-run local project tree. + +**Windows PowerShell** + +```powershell +powershell -ExecutionPolicy Bypass -c "$env:GLOBAL=1; irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" +``` + +**Linux / macOS** + +```bash +GLOBAL=1 bash -c "$(curl -fsSL http://fudankw.cn:9000/files/ga_install.sh)" +``` + +After installation, launch the desktop app from: + +```text +frontends/GenericAgent.exe +``` + +Or run from the project directory: + +```bash +python launch.pyw +``` + +> GenericAgent is meant to grow its environment through the Agent itself, not by pre-installing every possible package. Start small, then let GA install task-specific tools when it actually needs them. + +#### Custom install location + +```bash +INSTALL_DIR="$HOME/work/GenericAgent" GLOBAL=1 bash -c "$(curl -fsSL http://fudankw.cn:9000/files/ga_install.sh)" +``` + +```powershell +$env:INSTALL_DIR="C:\dev\GenericAgent"; powershell -ExecutionPolicy Bypass -c "$env:GLOBAL=1; irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" +``` + +#### Force reinstall + +Use this only when you know you want to refresh the installed files. Back up `mykey.py`, `memory/`, `skills/`, and any local work first. + +```bash +FORCE=1 GLOBAL=1 bash -c "$(curl -fsSL http://fudankw.cn:9000/files/ga_install.sh)" +``` + +### Method 2: Python install (for developers) + +Use this when you want a normal editable checkout. + +```bash +git clone https://github.com/lsdefine/GenericAgent.git +cd GenericAgent +uv venv +uv pip install -e ".[ui]" # Core + UI dependencies +cp mykey_template.py mykey.py # Fill in your LLM API key +python launch.pyw +``` + +Full guide: [GETTING_STARTED.md](GETTING_STARTED.md) + +### Configure your LLM key + +1. Open the installed `GenericAgent` directory. +2. If `mykey.py` does not exist, copy it from `mykey_template.py`. +3. Fill in one provider. Do **not** paste example keys as real keys. +4. If you are unsure about the fields, read the comments in `mykey_template.py` first. + +GA supports: + +- **OpenAI-compatible** endpoints — Chat Completions / Responses shaped APIs. +- **Anthropic Claude native** — Claude Messages API. + +Optional helper: + +```bash +python assets/configure_mykey.py +``` + +### Frontends + +#### Desktop App + +For one-line installs on Windows, double-click: + +```text +frontends/GenericAgent.exe +``` + +#### Terminal UI + +A lightweight keyboard-driven interface built on [Textual](https://github.com/Textualize/textual). It supports multiple concurrent sessions and real-time streaming. + +```bash +python frontends/tuiapp_v2.py +``` + +#### Streamlit UI + +```bash +python launch.pyw +``` + +### Verify the install + +From the GenericAgent directory: + +```bash +python -c "import agent_loop; print('OK')" +git rev-parse --short HEAD +``` + +Then launch at least one frontend: + +```bash +python launch.pyw +# or +python frontends/tuiapp_v2.py +``` + +### Common gotchas + +#### Python 3.14 is not supported + +If your system `python --version` reports 3.14, do not use it for GA. Use the one-line installer, or create a Python 3.11 / 3.12 environment with `uv`. + +#### `ga` command conflict + +Some systems already use `ga` for another tool. Check first: + +```bash +type ga +``` + +If it resolves to something unexpected, do not rely on the shortcut. Run GA from the install directory with `python launch.pyw` or `python frontends/tuiapp_v2.py`. + +#### Windows TUI rendering issues + +TUI rendering on Windows depends on terminal, font, and `textual` version. + +1. Upgrade Textual first: `pip install -U textual`. +2. Prefer **Git Bash on Windows** over classic PowerShell / cmd when Unicode or key bindings look broken. +3. If it still looks wrong, ask GA itself to repair the terminal compatibility. Example prompt: + > *"My experience using `frontends/tuiapp_v2.py` in PowerShell / cmd / Git Bash on Windows is very poor — lots of incompatibility. Please refer to Claude Code's best practices for the Windows terminal and fix all font and rendering incompatibilities."* + +#### `pywebview` install fails on Linux + +Desktop mode depends on GTK / WebKit packages. On Debian / Ubuntu: + +```bash +sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev +pip install pywebview +``` + +On headless servers, prefer the TUI or run Streamlit in browser mode instead of desktop mode. + +### Update an existing install + +Do not delete a working GA directory just to update it. It may contain the user's `mykey.py`, memory, skills, and local work. + +```bash +cd ~/GenericAgent +git fetch +git pull +uv pip install -e ".[ui]" +``` + +If `git pull` reports conflicts: + +- For `mykey.py`, `memory/`, `skills/`, `temp/`, and user-created artifacts: prefer the local copy. +- For core code such as `agent*.py`, `frontends/*`, `reflect/*`, and `tools/*`: usually prefer upstream unless the user says otherwise. +- Resolve explicitly, then `git add` and commit the merge. + +--- + +## For LLM Agents + +> **IMPORTANT: fetch this file with `curl`, not a summarized web reader.** Summaries often drop flags, command quoting, and safety rules. +> +> ```bash +> curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/refs/heads/main/docs/installation.md +> ``` + +You are installing **GenericAgent** for a human user. Be direct, verify each step, and avoid destructive cleanup unless the user explicitly authorizes it. + +### Step 0: Inspect before acting + +Check: + +1. OS: `uname -a`, `$env:OS`, or `python -c "import platform; print(platform.system())"`. +2. Existing install: `~/GenericAgent/launch.pyw`, `%USERPROFILE%\GenericAgent\launch.pyw`, or a `.git` directory. +3. Python version: must be 3.11 or 3.12 if you use the developer path. +4. Git availability: `git --version`. +5. API key readiness: ask the user if no key is available. Never invent one. + +### Step 1: Choose the install path + +Default to the one-line installer unless the user already has a checkout. + +**Windows PowerShell** + +```powershell +powershell -ExecutionPolicy Bypass -c "$env:GLOBAL=1; irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" +``` + +**Linux / macOS** + +```bash +GLOBAL=1 bash -c "$(curl -fsSL http://fudankw.cn:9000/files/ga_install.sh)" +``` + +If the user explicitly wants a developer checkout: + +```bash +git clone https://github.com/lsdefine/GenericAgent.git +cd GenericAgent +uv venv +uv pip install -e ".[ui]" +cp mykey_template.py mykey.py +``` + +### Step 2: Configure keys safely + +- Read `mykey_template.py` before editing `mykey.py`. +- Configure only keys the user actually provides. +- Never print the full key back to the user. +- If the user is unsure, run `python assets/configure_mykey.py` or ask which provider they want to use. + +Suggested question: + +> Which LLM should GenericAgent use? GA supports OpenAI-compatible endpoints and Anthropic Claude native APIs. Which provider do you want to configure, and do you already have an API key? + +### Step 3: Verify setup + +Run: + +```bash +cd ~/GenericAgent +python -c "import agent_loop; print('OK')" +git rev-parse --short HEAD +``` + +Then verify one frontend: + +```bash +python launch.pyw +# or, for terminal-only environments: +python frontends/tuiapp_v2.py +``` + +### Step 4: Verification and Next Steps + +Read this document again and think through whether you have done everything correctly: + +- You inspected the OS, existing install, Python version, Git, and key readiness before acting. +- You used the correct GA install command for this English guide, including `GLOBAL=1` where shown. +- You configured only real user-provided keys and did not reveal secrets. +- You verified import, Git commit hash, and at least one frontend. + +#### Say 'Congratulations! 🎉' to the user + +Say to user: Congratulations! 🎉 You have successfully set up GenericAgent! Double-click `frontends/GenericAgent.exe` on Windows, or run `python frontends/tuiapp_v2.py` in your terminal to start using it. + +#### Report the concrete result + +Tell the user: + +1. Install path. +2. Git commit hash. +3. Which frontend you verified. +4. Whether `mykey.py` is configured, without revealing secrets. +5. Recommended next reading: [Hello GenericAgent](https://datawhalechina.github.io/hello-generic-agent/) and [GETTING_STARTED.md](GETTING_STARTED.md). + +#### Ask for a Star ⭐ + +After everything is set up and working, you may ask: + +> If you found this helpful, would you like to star the GenericAgent repository? It helps the project grow! ⭐ + +Only open or run anything related to starring the repository if the user explicitly agrees. + +### Never do these + +- Do **not** run `pip install genericagent`; GA is not a PyPI package. +- Do **not** delete `~/GenericAgent` or `%USERPROFILE%\GenericAgent` for a "clean install" without explicit user approval. +- Do **not** fabricate API keys or treat sample keys as real. +- Do **not** assume `ga` is a valid shell command; check first. +- Do **not** rely on `ga update`; use `git fetch`, `git pull`, and reinstall dependencies as shown above. + +--- + +## References + +- Main README: [README.md](../README.md) +- Getting started: [GETTING_STARTED.md](GETTING_STARTED.md) +- Datawhale tutorial: +- Technical report: diff --git a/docs/installation_zh.md b/docs/installation_zh.md new file mode 100644 index 000000000..c124cc7b7 --- /dev/null +++ b/docs/installation_zh.md @@ -0,0 +1,324 @@ +# 安装指南(中文) + +这是 **GenericAgent** 的详细安装指南。 + +两类读者: + +- **[面向用户(For Humans)](#面向用户-for-humans)** —— 你自己安装 GA。 +- **[面向 LLM Agent(For LLM Agents)](#面向-llm-agent-for-llm-agents)** —— 你是 Claude Code、Codex 等编程 Agent,需要替人类用户安装 GA。请先读这一段,避免靠猜。 + +> 最短安装命令见主 [README](../README.md#-快速开始)。这份文档补充平台差异、Key 配置、验证、排障,以及 Agent 自动安装时的安全规则。 + +--- + +## 面向用户(For Humans) + +### 准备工作 + +| 要求 | 说明 | +|---|---| +| **操作系统** | Windows 10/11、macOS 12+,或任意现代 Linux。 | +| **Python** | 推荐 **Python 3.11 或 3.12**。**不要使用 Python 3.14**,它与 `pywebview` 及部分 GA 依赖不兼容。方法一的一键脚本会准备隔离运行环境,通常不需要手动装 Python。 | +| **Git** | 推荐安装,方便升级和自我进化。 | +| **LLM API Key** | GA 原生支持两类协议:**OpenAI 兼容接口** 和 **Anthropic Claude 原生接口**。GPT 系列、Claude、Kimi、MiniMax、DeepSeek、GLM、Qwen、通过 OAI 兼容网关接入的 Gemini 等,都可以在 `mykey.py` 中配置。 | + +### 方法一:一键安装(推荐) + +这是最省心的路径。脚本会准备隔离环境、下载 GenericAgent、安装核心依赖,并得到一个可以直接运行的本地项目目录。 + +**Windows PowerShell** + +```powershell +powershell -ExecutionPolicy Bypass -c "irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" +``` + +**Linux / macOS** + +```bash +curl -fsSL http://fudankw.cn:9000/files/ga_install.sh | bash +``` + +安装完成后,Windows 用户可双击: + +```text +frontends/GenericAgent.exe +``` + +也可以进入项目目录运行: + +```bash +python launch.pyw +``` + +> GenericAgent 更推荐由 Agent 在使用中自举环境,而不是预先手动装完整依赖。先把最小系统跑起来,需要什么工具再让 GA 自己安装。 + +#### 自定义安装路径 + +```bash +INSTALL_DIR="$HOME/work/GenericAgent" bash -c "$(curl -fsSL http://fudankw.cn:9000/files/ga_install.sh)" +``` + +```powershell +$env:INSTALL_DIR="C:\dev\GenericAgent"; powershell -ExecutionPolicy Bypass -c "irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" +``` + +#### 强制重新安装 + +仅在明确想刷新已安装文件时使用。请先备份 `mykey.py`、`memory/`、`skills/` 和本地工作成果。 + +```bash +FORCE=1 bash -c "$(curl -fsSL http://fudankw.cn:9000/files/ga_install.sh)" +``` + +### 方法二:Python 安装(开发者) + +适合想要可编辑源码目录的开发者。 + +```bash +git clone https://github.com/lsdefine/GenericAgent.git +cd GenericAgent +uv venv +uv pip install -e ".[ui]" # 核心 + UI 依赖 +cp mykey_template.py mykey.py # 填入你的 LLM API Key +python launch.pyw +``` + +完整引导流程见 [GETTING_STARTED.md](GETTING_STARTED.md)。 + +### 配置 LLM Key + +1. 打开已安装的 `GenericAgent` 目录。 +2. 如果没有 `mykey.py`,从 `mykey_template.py` 复制一份。 +3. 填入一个真实可用的模型服务商配置。**不要**把示例 Key 当真。 +4. 不确定字段含义时,先读 `mykey_template.py` 里的注释。 + +GA 支持: + +- **OpenAI 兼容接口** —— Chat Completions / Responses 形态的接口。 +- **Anthropic Claude 原生接口** —— Claude Messages API。 + +可选配置向导: + +```bash +python assets/configure_mykey.py +``` + +### 前端启动方式 + +#### 桌面端 + +一键安装自带桌面端,双击: + +```text +frontends/GenericAgent.exe +``` + +#### 终端 UI + +基于 [Textual](https://github.com/Textualize/textual) 的轻量键盘驱动界面。支持多会话并发、实时流式输出,有终端就能跑。 + +```bash +python frontends/tuiapp_v2.py +``` + +#### Streamlit UI + +```bash +python launch.pyw +``` + +### 验证安装 + +在 GenericAgent 目录下运行: + +```bash +python -c "import agent_loop; print('OK')" +git rev-parse --short HEAD +``` + +然后至少启动一个前端: + +```bash +python launch.pyw +# 或 +python frontends/tuiapp_v2.py +``` + +### 常见坑 + +#### 不支持 Python 3.14 + +如果系统 `python --version` 显示 3.14,不要用它跑 GA。请走一键安装,或用 `uv` 创建 Python 3.11 / 3.12 环境。 + +#### `ga` 命令冲突 + +有些系统已经把 `ga` 分配给其他工具。先检查: + +```bash +type ga +``` + +如果解析到意料之外的位置,就不要依赖这个快捷命令。请进入安装目录运行 `python launch.pyw` 或 `python frontends/tuiapp_v2.py`。 + +#### Windows 上 TUI 显示异常 + +TUI 在 Windows 上依赖终端、字体和 `textual` 版本。 + +1. 先升级 Textual:`pip install -U textual`。 +2. PowerShell / cmd 对 Unicode 和键位支持较弱,**Windows 上推荐用 Git Bash**。 +3. 仍然异常时,可以让 GA 自己修一遍。参考 Prompt: + > *"我在 Windows 的 PowerShell / cmd / Git Bash 中使用 `frontends/tuiapp_v2.py` 体验非常差,出现了一堆不兼容问题。请参考 Claude Code 在 Windows 终端的最佳配置,把所有字体和显示不兼容的问题修一遍。"* + +#### Linux 上 `pywebview` 安装失败 + +桌面模式依赖 GTK / WebKit。Debian / Ubuntu: + +```bash +sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev +pip install pywebview +``` + +无头服务器建议使用 TUI,或用浏览器方式运行 Streamlit,不要强行启桌面模式。 + +### 升级已有安装 + +不要为了升级而删除一个可用的 GA 目录。里面可能有用户的 `mykey.py`、记忆、技能和本地成果。 + +```bash +cd ~/GenericAgent +git fetch +git pull +uv pip install -e ".[ui]" +``` + +如果 `git pull` 报冲突: + +- `mykey.py`、`memory/`、`skills/`、`temp/` 和用户成果:本地优先。 +- `agent*.py`、`frontends/*`、`reflect/*`、`tools/*` 等核心代码:通常 upstream 优先,除非用户另有说明。 +- 明确解决冲突后,再 `git add` 并提交合并。 + +--- + +## 面向 LLM Agent(For LLM Agents) + +> **重要:用 `curl` 获取本文,不要依赖会摘要网页的读取器。** 摘要常会丢掉环境变量、命令引号和安全规则。 +> +> ```bash +> curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/refs/heads/main/docs/installation.md +> ``` + +你正在替人类用户安装 **GenericAgent**。请直接执行、逐步验证;除非用户明确授权,不要做破坏性清理。 + +### 第 0 步:先探测再行动 + +检查: + +1. 操作系统:`uname -a`、`$env:OS`,或 `python -c "import platform; print(platform.system())"`。 +2. 是否已安装:`~/GenericAgent/launch.pyw`、`%USERPROFILE%\GenericAgent\launch.pyw`,或 `.git` 目录。 +3. Python 版本:开发者路径必须使用 3.11 或 3.12。 +4. Git 是否可用:`git --version`。 +5. API Key 是否准备好:没有就问用户,绝不要伪造。 + +### 第 1 步:选择安装路径 + +除非用户已经有源码目录,否则默认走一键安装。 + +**Windows PowerShell** + +```powershell +powershell -ExecutionPolicy Bypass -c "irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" +``` + +**Linux / macOS** + +```bash +curl -fsSL http://fudankw.cn:9000/files/ga_install.sh | bash +``` + +如果用户明确要开发者源码安装: + +```bash +git clone https://github.com/lsdefine/GenericAgent.git +cd GenericAgent +uv venv +uv pip install -e ".[ui]" +cp mykey_template.py mykey.py +``` + +### 第 2 步:安全配置 Key + +- 编辑 `mykey.py` 前先读 `mykey_template.py`。 +- 只配置用户真实提供的 Key。 +- 不要把完整 Key 打印回给用户。 +- 用户不确定时,运行 `python assets/configure_mykey.py`,或询问要使用哪家模型服务商。 + +建议提问: + +> 你希望 GenericAgent 使用哪个 LLM?GA 支持 OpenAI 兼容接口和 Anthropic Claude 原生接口。你想配置哪家服务商,API Key 准备好了吗? + +### 第 3 步:验证安装 + +运行: + +```bash +cd ~/GenericAgent +python -c "import agent_loop; print('OK')" +git rev-parse --short HEAD +``` + +然后验证至少一个前端: + +```bash +python launch.pyw +# 或,无桌面环境时: +python frontends/tuiapp_v2.py +``` + +### 第 4 步:验证与下一步 + +重新读一遍本文,并思考你是否已经正确完成所有步骤: + +- 行动前检查了操作系统、已有安装、Python 版本、Git 和 Key 准备情况。 +- 使用了本文中文默认安装命令;不要擅自混入英文版的 `GLOBAL=1` 默认脚本。 +- 只配置了用户真实提供的 Key,且没有泄露密钥。 +- 已验证 import、Git commit hash,并至少验证了一个前端。 + +#### 对用户说 “Congratulations! 🎉” + +对用户说:Congratulations! 🎉 你已经成功安装 GenericAgent!Windows 下可双击 `frontends/GenericAgent.exe`,或在终端运行 `python frontends/tuiapp_v2.py` 开始使用。 + +#### 回报具体结果 + +告诉用户: + +1. 安装路径。 +2. Git commit hash。 +3. 已验证哪个前端。 +4. `mykey.py` 是否已配置,但不要泄露密钥。 +5. 下一步阅读:[Hello GenericAgent](https://datawhalechina.github.io/hello-generic-agent/) 和 [GETTING_STARTED.md](GETTING_STARTED.md)。 + +#### 请求 Star ⭐ + +确认安装可用后,可以询问: + +> 如果你觉得 GenericAgent 有帮助,愿意给仓库点一个 Star 吗?这会帮助项目成长!⭐ + +只有在用户明确同意后,才可以打开或执行任何与 Star 仓库相关的操作。 + +### 绝对不要做 + +- 不要运行 `pip install genericagent`;GA 不是 PyPI 包。 +- 未经明确授权,不要删除 `~/GenericAgent` 或 `%USERPROFILE%\GenericAgent` 做“干净安装”。 +- 不要伪造 API Key,也不要把示例 Key 当真。 +- 不要假设 `ga` 命令一定可用;先检查。 +- 不要依赖 `ga update`;按上面的 `git fetch`、`git pull` 和重装依赖流程做。 + +--- + +## 参考资料 + +- 主 README:[README.md](../README.md) +- Getting Started:[GETTING_STARTED.md](GETTING_STARTED.md) +- Datawhale 教程: +- 技术报告: +- English installation guide: [installation.md](installation.md) diff --git a/docs/macos_desktop_installation_zh.md b/docs/macos_desktop_installation_zh.md new file mode 100644 index 000000000..71b13fc8a --- /dev/null +++ b/docs/macos_desktop_installation_zh.md @@ -0,0 +1,85 @@ +# GenericAgent 桌面版安装指南 + +## 📦 安装步骤 + +### 第一步:打开安装包 + +双击下载的 `GenericAgent_x.x.x_aarch64.dmg` 文件,会弹出一个安装窗口。 + +将左边的 **GenericAgent** 图标拖到右边的 **Applications** 文件夹图标上,等待拷贝完成。 + +拷贝完成后,可以右键点击桌面上的 DMG 图标,选择「推出」来关闭安装包。 + +--- + +### 第二步:首次打开前的准备(重要) + +由于本应用暂未通过 Apple 官方签名认证,macOS 会阻止首次打开。这是正常的安全提示,不代表应用有问题。 + +请按以下步骤解除限制: + +#### 1. 打开「终端」应用 + +不知道终端是什么?别担心,它就是一个可以输入命令的工具。打开方式: + +- 按下键盘上的 `Command(⌘) + 空格键`,会弹出搜索框(Spotlight) +- 输入 `终端` 或 `Terminal`,按回车键打开 + +你会看到一个黑色或白色的文字窗口,里面有一个闪烁的光标,这就是终端。 + +#### 2. 输入解除限制的命令 + +在终端窗口中,复制粘贴以下这行命令(整行复制,一个字都不要漏��: + +``` +xattr -cr /Applications/GenericAgent.app +``` + +粘贴方法:在终端窗口里按 `Command(⌘) + V` + +然后按 `回车键(Enter)` 执行。 + +> 如果终端要求输入密码,输入你的 Mac 开机密码(输入时不会显示任何字符,这是正常的),然后按回车。 + +执行完毕后,终端不会有任何提示,这代表成功了。 + +#### 3. 打开 GenericAgent + +现在可以正常打开应用了: + +- 打开 Finder → 侧边栏点击「应用程序」 +- 找到 **GenericAgent**,双击打开 + +首次打开可能还会弹出一个确认框,点击「打开」即可。之后就不会再弹出了。 + +--- + +## ❓ 常见问题 + +### Q: 提示「GenericAgent 已损坏,无法打开」怎么办? + +这不是真的损坏,是 macOS 的安全机制。请回到第二步,确保在终端中执行了 `xattr -cr` 命令。 + +### Q: 提示「无法打开,因为无法验证开发者」怎么办? + +方法一(推荐):执行第二步的终端命令。 + +方法二:右键点击 GenericAgent.app → 选择「打开」→ 在弹出的对话框中点击「打开」。 + +### Q: 我的 Mac 是 Intel 芯片的,能用吗? + +当前版本仅支持 Apple Silicon(M1/M2/M3/M4)芯片的 Mac。如果你的 Mac 是 2020 年之前购买的,大概率是 Intel 芯片,暂时无法使用本安装包。 + +查看方法:点击左上角 → 「关于本机」,如果芯片一栏显示 Apple M1/M2/M3/M4,就可以使用。 + +### Q: 终端命令执行后没有任何反应? + +没有反应就是成功了。Unix/macOS 的设计哲学是「没有消息就是好消息」。 + +--- + +## 🔧 系统要求 + +- macOS 12 (Monterey) 或更高版本 +- Apple Silicon 芯片(M1/M2/M3/M4) +- 约 50MB 可用磁盘空间 diff --git a/frontends/DESKTOP_PET_README.md b/frontends/DESKTOP_PET_README.md new file mode 100644 index 000000000..1ee879875 --- /dev/null +++ b/frontends/DESKTOP_PET_README.md @@ -0,0 +1,175 @@ +# Desktop Pet Skin System + +## 快速开始 + +运行桌面宠物: +```bash +python3 desktop_pet_v2.pyw +``` + +## 功能特性 + +### 1. 多皮肤支持 +- 自动发现 `skins/` 目录下的所有皮肤 +- 右键菜单切换皮肤 +- 支持 sprite sheet 和 GIF 两种格式 + +### 2. 多动画状态 +- **idle** - 待机动画 +- **walk** - 行走动画 +- **run** - 跑步动画 +- **sprint** - 冲刺动画 + +右键菜单可切换动画状态 + +### 3. 交互功能 +- **单击** - 拖动宠物 +- **双击** - 关闭程序 +- **右键** - 打开菜单(切换皮肤/动画) + +### 4. HTTP 远程控制 +```bash +# 显示消息 +curl "http://127.0.0.1:51983/?msg=Hello" + +# 切换动画状态 +curl "http://127.0.0.1:51983/?state=run" + +# POST 消息 +curl -X POST -d "任务完成" http://127.0.0.1:51983/ +``` + +## 添加新皮肤 + +### 目录结构 +``` +skins/ +└── your-skin-name/ + ├── skin.json # 配置文件(必需) + ├── idle.png # 动画资源 + ├── walk.png + ├── run.png + └── sprint.png +``` + +### skin.json 配置示例 + +#### Sprite Sheet 格式(推荐) +```json +{ + "name": "My Pet", + "version": "1.0.0", + "author": "Your Name", + "description": "描述", + "format": "sprite", + "animations": { + "idle": { + "file": "idle.png", + "loop": true, + "sprite": { + "frameWidth": 44, + "frameHeight": 31, + "frameCount": 6, + "columns": 6, + "fps": 6, + "startFrame": 0 + } + }, + "walk": { + "file": "walk.png", + "loop": true, + "sprite": { + "frameWidth": 65, + "frameHeight": 32, + "frameCount": 8, + "columns": 8, + "fps": 8, + "startFrame": 0 + } + } + } +} +``` + +#### GIF 格式 +```json +{ + "name": "My Pet", + "format": "gif", + "animations": { + "idle": { + "file": "idle.gif", + "loop": true + }, + "walk": { + "file": "walk.gif", + "loop": true + } + } +} +``` + +### 配置说明 + +- **frameWidth/frameHeight**: 单帧尺寸(像素) +- **frameCount**: 帧数 +- **columns**: sprite sheet 的列数 +- **fps**: 播放帧率 +- **startFrame**: 起始帧索引(从 0 开始) + +### Sprite Sheet 布局 + +``` ++-------+-------+-------+-------+ +| 帧0 | 帧1 | 帧2 | 帧3 | ← 第一行 ++-------+-------+-------+-------+ +| 帧4 | 帧5 | 帧6 | 帧7 | ← 第二行 ++-------+-------+-------+-------+ +``` + +如果 `columns=4, startFrame=2, frameCount=3`,则读取:帧2, 帧3, 帧4 + +## 已包含的皮肤 + +1. **Glube** - 像素风小怪兽(多文件 sprite) +2. **Vita** - 像素风小恐龙(单文件 sprite) +3. **Doux** - 像素风小恐龙(单文件 sprite) + +## 从 ai-bubu 导入更多皮肤 + +ai-bubu 项目包含更多皮肤资源,可以直接复制: + +```bash +# 复制皮肤 +cp -r ai-bubu-main/packages/app/public/skins/boy frontends/skins/ +cp -r ai-bubu-main/packages/app/public/skins/dinosaur frontends/skins/ +cp -r ai-bubu-main/packages/app/public/skins/line frontends/skins/ +cp -r ai-bubu-main/packages/app/public/skins/mort frontends/skins/ +cp -r ai-bubu-main/packages/app/public/skins/tard frontends/skins/ +``` + +## 与 stapp.py 集成 + +在 `stapp.py` 中点击"🐱 桌面宠物"按钮会自动启动桌面宠物,并在每个 turn 结束时发送通知。 + +## 故障排查 + +### 皮肤不显示 +1. 检查 `skin.json` 格式是否正确 +2. 确认图片文件存在 +3. 检查 sprite 配置参数是否匹配图片尺寸 + +### 动画不流畅 +- 调整 `fps` 参数 +- 检查帧数是否正确 + +### 透明背景问题 +- 确保 PNG 文件包含 alpha 通道 +- 使用 RGBA 模式的图片 + +## 技术细节 + +- 基于 Tkinter + PIL/Pillow +- 支持透明背景(#01FF01 色键) +- 窗口置顶、无边框 +- HTTP 服务器端口:51983 diff --git a/frontends/at_complete.py b/frontends/at_complete.py new file mode 100644 index 000000000..3499652e4 --- /dev/null +++ b/frontends/at_complete.py @@ -0,0 +1,272 @@ +"""@ file completion — shared UI-less logic for tui_v2 / tui_v3. + +File index (os.scandir, cached per root) + fuzzy match + @token detection + +insert text. No UI deps; each front-end renders candidates its own way and +calls candidates_for(query, root). Index root is the front-end's choice +(session workspace, else CWD). Submit-time: completion-only does NOT read +content, but absolutize_mentions() rewrites @relative → @absolute so the +agent's file_read (relative to its own cwd) can locate the file. The +content-injecting auto-read variant lives in +temp/plan_v2_at_mention/autoread_version.py. +""" + +import os +import re +import threading + +# ---------------------------------------------------------------- index + +_IGNORE_DIRS = { + ".git", ".hg", ".svn", "node_modules", "__pycache__", ".venv", "venv", + ".mypy_cache", ".pytest_cache", ".ruff_cache", "dist", "build", + ".next", ".idea", ".vscode", "target", ".cache", ".eggs", + "model_responses", # GA 会话日志(上千个 .txt),未绑时根=temp 会淹没 @ 候选 +} +_IGNORE_EXT = {".pyc", ".pyo", ".so", ".o", ".class", ".lock", ".dll", ".exe"} +_MAX_FILES = 50_000 # 超大目录宁缺毋卡:到上限就停 + + +def scan_files(root: str, max_files: int = _MAX_FILES) -> list[str]: + """Collect relative file paths under root, '/'-normalized. + + os.scandir over os.walk: one syscall yields is_dir without an extra + stat per entry. Dotted dirs are skipped wholesale (.git, .venv...). + """ + out: list[str] = [] + stack = [root] + while stack and len(out) < max_files: + d = stack.pop() + try: + with os.scandir(d) as it: + for e in it: + try: + if e.is_dir(follow_symlinks=False): + if e.name not in _IGNORE_DIRS and not e.name.startswith("."): + stack.append(e.path) + elif e.is_file(follow_symlinks=False): + if os.path.splitext(e.name)[1].lower() not in _IGNORE_EXT: + rel = os.path.relpath(e.path, root).replace("\\", "/") + out.append(rel) + if len(out) >= max_files: + return out + except OSError: + continue + except OSError: + continue + return out + + +class FileIndexCache: + """Per-root background file index. warm() is idempotent-cheap: a + rebuild is only started when none is in flight.""" + + def __init__(self, root: str): + self.root = root + self._files: list[str] = [] + self._lock = threading.Lock() + self._building = False + self.ready = threading.Event() + + def warm(self) -> None: + with self._lock: + if self._building: + return + self._building = True + + def _build(): + try: + files = scan_files(self.root) + with self._lock: + self._files = files + self.ready.set() + finally: + with self._lock: + self._building = False + + threading.Thread(target=_build, name="ga-at-index", daemon=True).start() + + def snapshot(self) -> list[str]: + with self._lock: + return self._files + + +_indexes: dict[str, FileIndexCache] = {} +_indexes_lk = threading.Lock() + + +def get_index(root: str) -> FileIndexCache: + key = os.path.normcase(os.path.realpath(root or os.getcwd())) + with _indexes_lk: + idx = _indexes.get(key) + if idx is None: + idx = _indexes[key] = FileIndexCache(root) + return idx + + +# ---------------------------------------------------------------- fuzzy + +def _subseq_score(q: str, path: str): + """Subsequence match score (higher = better), None when q doesn't + fully appear in order. Contiguous runs dominate (fzf-style): scattered + one-char hits across a long path must not beat a tight cluster. + Word-boundary hits and basename substring add on top; ties broken by + caller on shorter path.""" + if not q: + return 0 + score, qi, prev_hit = 0, 0, -2 + for pi, ch in enumerate(path): + if qi < len(q) and ch == q[qi]: + score += 1 + if pi == prev_hit + 1: + score += 2 # contiguous run: the dominant signal + if pi == 0 or path[pi - 1] in "/\\_-. ": + score += 3 + prev_hit = pi + qi += 1 + if qi < len(q): + return None + base = path.rsplit("/", 1)[-1] + if q in base: + score += 8 + elif q in path: + score += 4 + return score + + +def fuzzy_rank(query: str, files: list[str], limit: int = 10) -> list[str]: + q = query.lower() + if not q: + # bare `@`: surface shallow paths first for discoverability + return sorted(files, key=lambda f: (f.count("/"), f))[:limit] + scored = [] + for f in files: + s = _subseq_score(q, f.lower()) + if s is not None: + scored.append((s, f)) + scored.sort(key=lambda x: (-x[0], len(x[1]), x[1])) + return [f for _, f in scored[:limit]] + + +# ------------------------------------------------------- edit-time token + +# `(?:^|\s)@` 前置:@ 前必须是行首或空白 → 邮箱/代码里的 a@b 不触发。 +# 字符集含路径分隔符与 ~ :,\w 在 unicode 下覆盖中文文件名。 +_AT_TOKEN_RE = re.compile(r"(?:^|\s)(@[\w\-./\\~:]*)$", re.UNICODE) + + +def find_at_token(line_before_cursor: str): + """Return (query, at_pos) when the cursor sits in an @token being + typed on this line, else None. at_pos is the index of '@'.""" + m = _AT_TOKEN_RE.search(line_before_cursor) + if not m: + return None + tok = m.group(1) + return tok[1:], m.start(1) + + +def format_pick(path: str) -> str: + """`@path` insert text; dirs get no trailing space (keep completing next + level), files get one (close token). Spaces → quoted.""" + trailing = '' if path.endswith(('/', '\\')) else ' ' + return f'@"{path}"{trailing}' if ' ' in path else f'@{path}{trailing}' + + +# --- path-like completion: an explicit-path @token (~/ / ./ ../ or C:\) goes +# to live directory completion instead of index fuzzy — this is how absolute +# paths outside the index root get completed level by level (claude-code parity). + +def is_path_like(token: str) -> bool: + if token in ('~', '.', '..'): + return True + if token.startswith(('~/', '~\\', './', '.\\', '../', '..\\', '/', '\\')): + return True + return len(token) >= 3 and token[0].isalpha() and token[1] == ':' and token[2] in '/\\' + + +def path_completions(token: str, root: str, limit: int = 15) -> list[str]: + """readdir the real dir of a path-like token, prefix-match, dirs first. + `~` expanded, relative → root, absolute as-is; candidates keep the token's + spelling, dirs carry a trailing '/'.""" + sep = max(token.rfind('/'), token.rfind('\\')) + if sep >= 0: + dir_part, prefix = token[:sep + 1], token[sep + 1:] + elif token in ('~', '.', '..'): + dir_part, prefix = token.rstrip('/\\') + '/', '' + else: + return [] + exp = os.path.expanduser(dir_part) + real_dir = exp if os.path.isabs(exp) else os.path.join(root, exp) + try: + with os.scandir(real_dir) as it: + entries = list(it) + except OSError: + return [] + pl = prefix.lower() + rows = [] + for e in entries: + nm = e.name + if pl and not nm.lower().startswith(pl): + continue + if nm.startswith('.') and not prefix.startswith('.'): # 隐藏项需显式 . 才出 + continue + try: + is_dir = e.is_dir() + except OSError: + is_dir = False + rows.append((not is_dir, nm.lower(), dir_part + nm + ('/' if is_dir else ''))) + rows.sort(key=lambda r: (r[0], r[1])) # 目录优先 + 字母序 + return [d for _, _, d in rows[:limit]] + + +def candidates_for(query: str, root: str, limit: int = 15, absolute: bool = False) -> list[str]: + """@token candidates: path-like → directory completion, else index fuzzy. + Single dispatch point shared by both front-ends. `absolute=True` returns + fuzzy hits as absolute paths (front-end shows full path when no workspace + is bound, since the relative root isn't obvious to the user).""" + if is_path_like(query): + return path_completions(query, root, limit) + idx = get_index(root) + files = idx.snapshot() + if not files: + idx.warm() # 惰性兜底:该根还没建索引 → 后台建(本次可能空,下次有) + res = fuzzy_rank(query, files, limit) if files else [] + if absolute: + res = [os.path.normpath(os.path.join(root, c)) for c in res] + return res + + +# ------------------------------------------------------ submit-time absolutize +# A fuzzy candidate inserts a path relative to the @ root (workspace/CWD), but +# the agent's file_read resolves relative to its own ./temp cwd — so a bare +# `@frontends/x.py` won't be found. At submit we rewrite each @mention naming a +# real file to an absolute path; display keeps the short form. Still no content +# read — this only completes the path so the agent can locate it. + +_AT_ABS_RE = re.compile(r'(^|\s)@("([^"]+)"|([\w\-./\\~:#]+))', re.UNICODE) +_LINE_SUFFIX_RE = re.compile(r'(#L\d+(?:-\d+)?)$') + + +def absolutize_mentions(text: str, root: str) -> str: + """@relative → @absolute (root-resolved, ~ expanded, quoted if it gains a + space), `#Lx-y` suffix kept. Only existing paths are rewritten; decorative + @words / typos pass through unchanged.""" + def repl(m): + lead, quoted, bare = m.group(1), m.group(3), m.group(4) + raw = quoted if quoted is not None else bare + trail = '' + if quoted is None: # strip trailing prose punctuation + stripped = raw.rstrip(',。,;;))]》>') + trail, raw = raw[len(stripped):], stripped + sm = _LINE_SUFFIX_RE.search(raw) + suffix = sm.group(1) if sm else '' + path = raw[:len(raw) - len(suffix)] if suffix else raw + if not path: + return m.group(0) + exp = os.path.expanduser(path) + absp = os.path.normpath(exp if os.path.isabs(exp) else os.path.join(root, exp)) + if not os.path.exists(absp): # decorative / typo → leave as-is + return m.group(0) + full = absp + suffix + token = f'@"{full}"' if ' ' in full else f'@{full}' + return lead + token + trail + return _AT_ABS_RE.sub(repl, text) diff --git a/frontends/btw_cmd.py b/frontends/btw_cmd.py new file mode 100644 index 000000000..fca03b45d --- /dev/null +++ b/frontends/btw_cmd.py @@ -0,0 +1,142 @@ +"""`/btw` 命令:side question — 不打断主 Agent 的临时 subagent 问答。 + +- 持锁 deepcopy backend.history → 后台线程 backend.raw_ask 单次拉答 +- 主 agent backend.history 零写入;不入 task_queue +- 答案 → display_queue 'done'(install 路径)或同步 return(frontend 路径) + +复用 backend.raw_ask + make_messages,不新建 LLM 实例。 +""" +from __future__ import annotations +import copy, os, threading, time +from typing import Optional + + +_WRAPPER_ZH = """ +这是用户的临时插问 (side question)。主 agent 仍在后台运行,**不会被打断**。 + +身份与边界: +- 你是一个独立的轻量 sub-agent +- 上下文里能看到主 agent 与用户的完整对话、最近的工具调用与结果 +- 用户在问当前进展或顺便确认某事——基于已有信息**一次性**作答 +- 没有任何工具可用:不要"让我查一下" / "我去试试" / 任何承诺动作 +- 信息不足就坦白说"基于目前对话我不知道" + +侧问内容如下: + + +{question}""" + +_WRAPPER_EN = """ +This is a side question from the user. The main agent is NOT interrupted — it continues in the background. + +Identity & boundaries: +- You are an independent lightweight sub-agent +- You can see the full conversation between the main agent and the user, plus recent tool calls/results +- The user is asking about current progress or a quick aside — answer in **one shot** from existing info +- You have NO tools — never say "let me check" / "I'll try" / any action promise +- If info is missing, just say "based on the conversation I don't know" + +Question: + + +{question}""" + +_TIMEOUT_SEC = 120 + + +def _wrapper(): return _WRAPPER_EN if os.environ.get('GA_LANG') == 'en' else _WRAPPER_ZH + + +def _strip_cmd(query): + s = (query or '').strip() + return s[len('/btw'):].strip() if s.startswith('/btw') else s + + +def _help_text(): + return ('**/btw 用法**:side question — 临时问主 agent 当前进展,不打断主线\n\n' + '`/btw <你的问题>`\n\n' + '行为:抓取当前对话上下文 → 单轮纯文本作答(无工具)→ 主 agent 历史不变。') + + +def _snapshot_history(backend): + """Lock + deepcopy: defends against concurrent compress_history_tags mutating inner blocks.""" + with backend.lock: + return copy.deepcopy(list(backend.history)) + + +def _build_wire(backend, history, sidequest_msg): + """history + sidequest → wire-format. Dispatches: BaseSession subclasses → make_messages, + Native* → raw pairs (raw_ask runs _fix/_drop/_ensure transforms itself).""" + msgs = history + [sidequest_msg] + if hasattr(backend, 'make_messages'): + return backend.make_messages(msgs) + return [{"role": m["role"], "content": list(m.get("content", []))} for m in msgs] + + +def _ask(agent, question, deadline): + """One-shot raw_ask against current backend; never mutates backend.history.""" + backend = agent.llmclient.backend + user_msg = {"role": "user", + "content": [{"type": "text", "text": _wrapper().format(question=question)}]} + wire = _build_wire(backend, _snapshot_history(backend), user_msg) + text = '' + for chunk in backend.raw_ask(wire): + text += chunk + if time.time() > deadline: + return text + '\n\n⚠️ /btw 超时,仅返回部分回复。' + return text + + +def _format(question, body, took): + head = f'> 🟡 /btw {question}\n\n' + return head + (body.strip() or '*(空回复)*') + f'\n\n*({took:.1f}s)*' + + +def _run(agent, question, deadline): + """Catches errors at the boundary so neither caller path needs its own try/except.""" + try: return _ask(agent, question, deadline) + except Exception as e: return f'❌ /btw 失败: {type(e).__name__}: {e}' + + +def handle(agent, query, display_queue) -> Optional[str]: + """Slash-cmd entry (server-side, install path). Spawn worker; return None to consume.""" + question = _strip_cmd(query) + if not question or question in ('help', '?', '-h', '--help'): + display_queue.put({'done': _help_text(), 'source': 'system'}) + return None + started = time.time() + deadline = started + _TIMEOUT_SEC + + def worker(): + body = _run(agent, question, deadline) + display_queue.put({'done': _format(question, body, time.time() - started), 'source': 'system'}) + + threading.Thread(target=worker, daemon=True, name='btw-sidequest').start() + return None + + +def handle_frontend_command(agent, query) -> str: + """Sync entry for frontends wanting a string back (tg/wx/stapp/...).""" + question = _strip_cmd(query) + if not question or question in ('help', '?', '-h', '--help'): + return _help_text() + started = time.time() + body = _run(agent, question, started + _TIMEOUT_SEC) + return _format(question, body, time.time() - started) + + +def install(cls): + """Idempotent monkey-patch: intercept /btw before original dispatch.""" + orig = cls._handle_slash_cmd + if getattr(orig, '_btw_patched', False): return + + def patched(self, raw_query, display_queue): + s = (raw_query or '').strip() + if s == '/btw' or s.startswith('/btw ') or s.startswith('/btw\t'): + r = handle(self, raw_query, display_queue) + if r is None: return None + return r + return orig(self, raw_query, display_queue) + + patched._btw_patched = True + cls._handle_slash_cmd = patched diff --git a/frontends/chat_bubble.png b/frontends/chat_bubble.png new file mode 100644 index 000000000..c1969df15 Binary files /dev/null and b/frontends/chat_bubble.png differ diff --git a/frontends/chatapp_common.py b/frontends/chatapp_common.py new file mode 100644 index 000000000..befaf1c8d --- /dev/null +++ b/frontends/chatapp_common.py @@ -0,0 +1,352 @@ +import ast, asyncio, glob, json, os, queue as Q, re, socket, sys, time + +# 确保能导入上级目录的模块(如 agentmain) +_parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if _parent_dir not in sys.path: + sys.path.insert(0, _parent_dir) + +HELP_COMMANDS = ( + ("/help", "显示帮助"), + ("/status", "查看状态"), + ("/stop", "停止当前任务"), + ("/new", "开启新对话并清空当前上下文"), + ("/restore", "恢复上次对话历史"), + ("/continue", "列出可恢复会话"), + ("/continue [n]", "恢复第 n 个会话"), + ("/btw ", "side question — 临时插问主 agent 进展,不打断主线"), + ("/review [scope]", "in-session code review; 默认审当前 git diff"), + ("/llm", "查看当前模型列表"), + ("/llm [n]", "切换到第 n 个模型"), +) +TELEGRAM_MENU_COMMANDS = ( + ("help", "显示帮助"), + ("status", "查看状态"), + ("stop", "停止当前任务"), + ("new", "开启新对话并清空当前上下文"), + ("restore", "恢复上次对话历史"), + ("continue", "列出可恢复会话;/continue n 恢复第 n 个"), + ("btw", "临时插问主 agent 进展,不打断主线"), + ("review", "in-session code review;/review scope 指定范围"), + ("llm", "查看模型列表;/llm n 切换到指定模型"), +) + + +def build_help_text(commands=HELP_COMMANDS): + return "📖 命令列表:\n" + "\n".join(f"{cmd} - {desc}" for cmd, desc in commands) + + +HELP_TEXT = build_help_text() +FILE_HINT = "If you need to show files to user, use [FILE:filepath] in your response." +TAG_PATS = [r"<" + t + r">.*?" for t in ("thinking", "summary", "tool_use", "file_content")] +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +RESTORE_GLOBS = ( + os.path.join(PROJECT_ROOT, "temp", "model_responses", "model_responses_*.txt"), + os.path.join(PROJECT_ROOT, "temp", "model_responses_*.txt"), +) +RESTORE_BLOCK_RE = re.compile( + r"^=== (Prompt|Response) ===.*?\n(.*?)(?=^=== (?:Prompt|Response) ===|\Z)", + re.DOTALL | re.MULTILINE, +) +HISTORY_RE = re.compile(r"\s*(.*?)\s*", re.DOTALL) +SUMMARY_RE = re.compile(r"\s*(.*?)\s*", re.DOTALL) + + +def clean_reply(text): + for pat in TAG_PATS: + text = re.sub(pat, "", text or "", flags=re.DOTALL) + return re.sub(r"\n{3,}", "\n\n", text).strip() or "..." + + +def extract_files(text): + return re.findall(r"\[FILE:([^\]]+)\]", text or "") + + +def strip_files(text): + return re.sub(r"\[FILE:[^\]]+\]", "", text or "").strip() + + +def split_text(text, limit): + text, parts = (text or "").strip() or "...", [] + while len(text) > limit: + cut = text.rfind("\n", 0, limit) + if cut < limit * 0.6: + cut = limit + parts.append(text[:cut].rstrip()) + text = text[cut:].lstrip() + return parts + ([text] if text else []) or ["..."] + + +def _restore_log_files(): + files = [] + for pattern in RESTORE_GLOBS: + files.extend(glob.glob(pattern)) + return sorted(set(files)) + + +def _restore_text_pairs(content): + users = re.findall(r"=== USER ===\n(.+?)(?==== |$)", content, re.DOTALL) + resps = re.findall(r"=== Response ===.*?\n(.+?)(?==== Prompt|$)", content, re.DOTALL) + restored = [] + for u, r in zip(users, resps): + u, r = u.strip(), r.strip()[:500] + if u and r: + restored.extend([f"[USER]: {u}", f"[Agent] {r}"]) + return restored + + +def _native_prompt_obj(prompt_body): + try: + prompt = json.loads(prompt_body) + except Exception: + return None + if not isinstance(prompt, dict) or prompt.get("role") != "user": + return None + if not isinstance(prompt.get("content"), list): + return None + return prompt + + +def _native_prompt_text(prompt): + texts = [] + for block in prompt.get("content", []): + if isinstance(block, dict) and block.get("type") == "text": + text = block.get("text", "") + if isinstance(text, str) and text.strip(): + texts.append(text) + return "\n".join(texts).strip() + + +def _native_history_lines(prompt_text): + match = HISTORY_RE.search(prompt_text or "") + if not match: + return [] + restored = [] + for line in match.group(1).splitlines(): + line = line.strip() + if line.startswith("[USER]: ") or line.startswith("[Agent] "): + restored.append(line) + return restored + + +def _native_first_user_line(prompt_text): + text = (prompt_text or "").strip() + if not text or "" in text or text.startswith("### [WORKING MEMORY]"): + return "" + if text.startswith(FILE_HINT): + text = text[len(FILE_HINT):].lstrip() + if "### 用户当前消息" in text: + text = text.split("### 用户当前消息", 1)[-1].strip() + return text + + +def _native_response_summary(response_body): + try: + blocks = ast.literal_eval((response_body or "").strip()) + except Exception: + return "" + if not isinstance(blocks, list): + return "" + text_parts = [] + for block in blocks: + if isinstance(block, dict) and block.get("type") == "text": + text = block.get("text", "") + if isinstance(text, str) and text: + text_parts.append(text) + match = SUMMARY_RE.search("\n".join(text_parts)) + return (match.group(1).strip() if match else "")[:500] + + +def _restore_native_history(content): + blocks = RESTORE_BLOCK_RE.findall(content or "") + if not blocks: + return [] + pairs = [] + pending_prompt = None + for label, body in blocks: + if label == "Prompt": + pending_prompt = body + elif pending_prompt is not None: + pairs.append((pending_prompt, body)) + pending_prompt = None + for prompt_body, response_body in reversed(pairs): + prompt = _native_prompt_obj(prompt_body) + if prompt is None: + continue + prompt_text = _native_prompt_text(prompt) + restored = list(_native_history_lines(prompt_text)) + if restored: + summary = _native_response_summary(response_body) + summary_line = f"[Agent] {summary}" if summary else "" + if summary_line and (not restored or restored[-1] != summary_line): + restored.append(summary_line) + return restored + user_text = _native_first_user_line(prompt_text) + summary = _native_response_summary(response_body) + if user_text and summary: + return [f"[USER]: {user_text}", f"[Agent] {summary}"] + return [] + + +def format_restore(): + files = _restore_log_files() + if not files: + return None, "❌ 没有找到历史记录" + latest = max(files, key=os.path.getmtime) + with open(latest, "r", encoding="utf-8") as f: + content = f.read() + restored = _restore_text_pairs(content) or _restore_native_history(content) + if not restored: + return None, "❌ 历史记录里没有可恢复内容" + count = sum(1 for line in restored if line.startswith("[USER]: ")) + return (restored, os.path.basename(latest), count), None + + +def build_done_text(raw_text): + files = [p for p in extract_files(raw_text) if os.path.exists(p)] + body = strip_files(clean_reply(raw_text)) + if files: + body = (body + "\n\n" if body else "") + "\n".join(f"生成文件: {p}" for p in files) + return body or "..." + + +def public_access(allowed): + return not allowed or "*" in allowed + + +def to_allowed_set(value): + if value is None: + return set() + if isinstance(value, str): + value = [value] + return {str(x).strip() for x in value if str(x).strip()} + + +def allowed_label(allowed): + return "public" if public_access(allowed) else sorted(allowed) + + +def ensure_single_instance(port, label): + try: + lock_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + lock_sock.bind(("127.0.0.1", port)) + return lock_sock + except OSError: + print(f"[{label}] Another instance is already running, skipping...") + sys.exit(1) + + +def require_runtime(agent, label, **required): + missing = [k for k, v in required.items() if not v] + if missing: + print(f"[{label}] ERROR: please set {', '.join(missing)} in mykey.py or mykey.json") + sys.exit(1) + if agent.llmclient is None: + print(f"[{label}] ERROR: no usable LLM backend found in mykey.py or mykey.json") + sys.exit(1) + + +def redirect_log(script_file, log_name, label, allowed): + log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(script_file))), "temp") + os.makedirs(log_dir, exist_ok=True) + logf = open(os.path.join(log_dir, log_name), "a", encoding="utf-8", buffering=1) + sys.stdout = sys.stderr = logf + print(f"[NEW] {label} process starting, the above are history infos ...") + print(f"[{label}] allow list: {allowed_label(allowed)}") + + +class AgentChatMixin: + label = "Chat" + source = "chat" + split_limit = 1500 + ping_interval = 20 + + def __init__(self, agent, user_tasks): + self.agent, self.user_tasks = agent, user_tasks + + async def send_text(self, chat_id, content, **ctx): + raise NotImplementedError + + async def send_done(self, chat_id, raw_text, **ctx): + await self.send_text(chat_id, build_done_text(raw_text), **ctx) + + async def handle_command(self, chat_id, cmd, **ctx): + parts = (cmd or "").split() + op = (parts[0] if parts else "").lower() + if op == "/help": + return await self.send_text(chat_id, HELP_TEXT, **ctx) + if op == "/stop": + state = self.user_tasks.get(chat_id) + if state: + state["running"] = False + self.agent.abort() + return await self.send_text(chat_id, "⏹️ 正在停止...", **ctx) + if op == "/status": + llm = self.agent.get_llm_name() if self.agent.llmclient else "未配置" + return await self.send_text(chat_id, f"状态: {'🔴 运行中' if self.agent.is_running else '🟢 空闲'}\nLLM: [{self.agent.llm_no}] {llm}", **ctx) + if op == "/llm": + if not self.agent.llmclient: + return await self.send_text(chat_id, "❌ 当前没有可用的 LLM 配置", **ctx) + if len(parts) > 1: + try: + self.agent.next_llm(int(parts[1])) + return await self.send_text(chat_id, f"✅ 已切换到 [{self.agent.llm_no}] {self.agent.get_llm_name()}", **ctx) + except Exception: + return await self.send_text(chat_id, f"用法: /llm <0-{len(self.agent.list_llms()) - 1}>", **ctx) + lines = [f"{'→' if cur else ' '} [{i}] {name}" for i, name, cur in self.agent.list_llms()] + return await self.send_text(chat_id, "LLMs:\n" + "\n".join(lines), **ctx) + if op == "/restore": + try: + restored_info, err = format_restore() + if err: + return await self.send_text(chat_id, err, **ctx) + restored, fname, count = restored_info + self.agent.abort() + self.agent.history.extend(restored) + return await self.send_text(chat_id, f"✅ 已恢复 {count} 轮对话\n来源: {fname}\n(仅恢复上下文,请输入新问题继续)", **ctx) + except Exception as e: + return await self.send_text(chat_id, f"❌ 恢复失败: {e}", **ctx) + if op == "/continue": + return await self.send_text(chat_id, _handle_continue_frontend(self.agent, cmd), **ctx) + if op == "/new": + return await self.send_text(chat_id, _reset_conversation(self.agent), **ctx) + if op == "/btw": + answer = await asyncio.to_thread(_handle_btw_frontend, self.agent, cmd) + return await self.send_text(chat_id, answer, **ctx) + if op == "/review": + return await self.run_agent(chat_id, cmd, **ctx) + return await self.send_text(chat_id, HELP_TEXT, **ctx) + + async def run_agent(self, chat_id, text, **ctx): + state = {"running": True} + self.user_tasks[chat_id] = state + try: + await self.send_text(chat_id, "思考中...", **ctx) + dq = self.agent.put_task(f"{FILE_HINT}\n\n{text}", source=self.source) + last_ping = time.time() + while state["running"]: + try: + item = await asyncio.to_thread(dq.get, True, 3) + except Q.Empty: + if self.agent.is_running and time.time() - last_ping > self.ping_interval: + await self.send_text(chat_id, "⏳ 还在处理中,请稍等...", **ctx) + last_ping = time.time() + continue + if "done" in item: + await self.send_done(chat_id, item.get("done", ""), **ctx) + break + if not state["running"]: + await self.send_text(chat_id, "⏹️ 已停止", **ctx) + except Exception as e: + import traceback + print(f"[{self.label}] run_agent error: {e}") + traceback.print_exc() + await self.send_text(chat_id, f"❌ 错误: {e}", **ctx) + finally: + self.user_tasks.pop(chat_id, None) + + +from agentmain import GeneraticAgent as _GA +from continue_cmd import handle_frontend_command as _handle_continue_frontend, install as _install_continue, reset_conversation as _reset_conversation +_install_continue(_GA) +from btw_cmd import handle_frontend_command as _handle_btw_frontend, install as _install_btw; _install_btw(_GA) +from review_cmd import install as _install_review; _install_review(_GA) diff --git a/frontends/conductor.html b/frontends/conductor.html new file mode 100644 index 000000000..5380d5c8d --- /dev/null +++ b/frontends/conductor.html @@ -0,0 +1,513 @@ + + + + + + Conductor + + + +
    +
    + +
    + +
    +
    +
    Subagents
    +
    点击卡片展开 · live
    +
    +
    +
    暂无 subagent。
    +
    +
    + + +
    +
    + 🧠 Conductor 输出 + +
    +
    +
    +
    + + +
    + +
    +
    +
    待批任务 0
    +
    conductor 拟派 subagent · 待你批复
    +
    +
    +
    + + +
    +
    +
    Conversation
    +
    connecting...
    +
    +
    +
    + + +
    +
    +
    +
    +
    + + + + + diff --git a/frontends/conductor.py b/frontends/conductor.py new file mode 100644 index 000000000..79f5ac8fb --- /dev/null +++ b/frontends/conductor.py @@ -0,0 +1,655 @@ +import os, sys, re, time, json, uuid, queue, asyncio, threading +from dataclasses import dataclass, field +from typing import Dict, Any, Optional, List +from contextlib import asynccontextmanager, suppress + +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import FileResponse, PlainTextResponse, JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if ROOT not in sys.path: sys.path.insert(0, ROOT) + +from agentmain import GenericAgent + +HOST = "127.0.0.1" +PORT = 8900 +HTML_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "conductor.html") + + +def _settings_doc() -> dict: + try: + from pathlib import Path + doc = json.loads((Path.home() / ".ga_desktop_settings.json").read_text(encoding="utf-8")) + return doc if isinstance(doc, dict) else {} + except Exception: + return {} + + +def _conductor_llm_no() -> Optional[int]: + """Read the model index bound to the conductor session. + Falls back to the legacy desktop default ui.llmNo for existing installs.""" + doc = _settings_doc() + for section in (doc.get("conductor"), doc.get("ui")): + if isinstance(section, dict) and section.get("llmNo") is not None: + try: + return int(section.get("llmNo")) + except (TypeError, ValueError): + pass + return None + + +def _client_usable(agent: "GenericAgent") -> bool: + return hasattr(getattr(agent, "llmclient", None), "backend") + + +def _parse_model_no(value: Any) -> Optional[int]: + if value is None or isinstance(value, bool): + return None + try: + return int(value) + except (TypeError, ValueError): + return None + + +def _usable_model(agent: "GenericAgent", no: Optional[int]) -> bool: + clients = getattr(agent, "llmclients", []) or [] + return no is not None and 0 <= no < len(clients) and hasattr(clients[no], "backend") + + +def _activate_model(agent: "GenericAgent", no: int) -> None: + if not _usable_model(agent, no): + raise ValueError(f"llm index out of range or unavailable: {no}") + agent.next_llm(no) + + +def _runtime_model_state(agent: "GenericAgent", configured: Optional[int], reason: Optional[str]) -> dict: + effective = getattr(agent, "llm_no", None) if _client_usable(agent) else None + current = None + if _client_usable(agent): + with suppress(Exception): + current = str(agent.llmclient.backend.name) + return {"configured": configured, "effective": effective, + "fallbackReason": reason, "current": current} + + +def _apply_desktop_model(agent: "GenericAgent") -> dict: + """Make the conductor's session reflect the current desktop config before a task: + switch to its bound model if one is set, otherwise still refresh sessions from + mykey so live key/model edits (e.g. importing keys) take effect without a restart. + next_llm() already reloads internally; the no-bound-model branch must reload too, + or a conductor started on an empty/stale mykey would never pick up imported keys.""" + doc = _settings_doc() + conductor_cfg = doc.get("conductor") if isinstance(doc.get("conductor"), dict) else {} + ui_cfg = doc.get("ui") if isinstance(doc.get("ui"), dict) else {} + raw_configured = conductor_cfg.get("llmNo") + configured = _parse_model_no(raw_configured) + ui_default = _parse_model_no(ui_cfg.get("llmNo")) + + try: + agent.load_llm_sessions() # mtime-guarded; rebuilds only when mykey changed + except Exception as e: + print(f"[conductor] failed to refresh model sessions: {e}", file=sys.stderr) + + clients = getattr(agent, "llmclients", []) or [] + + configured_failed = False + if _usable_model(agent, configured): + try: + _activate_model(agent, configured) + return _runtime_model_state(agent, configured, None) + except Exception as e: + configured_failed = True + print(f"[conductor] configured model #{configured} is unavailable: {e}", file=sys.stderr) + + if _usable_model(agent, ui_default): + if raw_configured is None: + reason = "ui_default" + elif configured_failed: + reason = "configured_unavailable" + elif configured is not None: + reason = "configured_unavailable" if 0 <= configured < len(clients) else "invalid_configured" + else: + reason = "invalid_configured" + try: + _activate_model(agent, ui_default) + return _runtime_model_state(agent, configured, reason) + except Exception as e: + print(f"[conductor] UI default model #{ui_default} is unavailable: {e}", file=sys.stderr) + + for i, client in enumerate(clients): + if hasattr(client, "backend"): + try: + _activate_model(agent, i) + return _runtime_model_state(agent, configured, "first_available") + except Exception: + continue + + print("[conductor] no usable model is available", file=sys.stderr) + return {"configured": configured, "effective": None, + "fallbackReason": "no_models", "current": None} + + +def _select_llm(agent: "GenericAgent", llm: Any) -> bool: + if llm is None or str(llm).strip() == "": return False + q = str(llm).strip() + if isinstance(llm, int) or q.isdigit(): + agent.load_llm_sessions() + _activate_model(agent, int(q)) + return True + q = q.lower(); agent.load_llm_sessions() + for i, c in enumerate(agent.llmclients): + if not hasattr(c, "backend"): + continue + vals = [] + for fn in (lambda: agent.get_llm_name(c), lambda: agent.get_llm_name(c, model=True), lambda: c.backend.name, lambda: c.backend.model): + try: vals.append(str(fn()).lower()) + except Exception: pass + if any(q in v for v in vals): _activate_model(agent, i); return True + raise ValueError(f"llm not found: {llm}") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # 服务启动(事件循环已就绪):捕获 loop 供工作线程跨线程推 WS 广播,并起主agent + global main_loop + main_loop = asyncio.get_running_loop() + import cost_tracker; cost_tracker.install() + conductor.start() + threading.Thread(target=im_poll_loop, name="im-poller", daemon=True).start() + yield + + +app = FastAPI(title="Conductor", lifespan=lifespan) +app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) + +class ChatIn(BaseModel): + msg: str + role: str = "conductor" # conductor | system | user + +class StartSubagentIn(BaseModel): + prompt: str + llm: Any = None + +class ApprovalIn(BaseModel): + prompt: str + source: str = "" + +class SubagentActionIn(BaseModel): + action: str = "intervene" # intervene | abort | kill + msg: str = "" + llm: Any = None + +@dataclass +class SubAgentState: + id: str + agent: GenericAgent + prompt: str + thread: Optional[threading.Thread] = None + reply: str = "" + status: str = "running" # running | stopped + created_at: int = field(default_factory=lambda: int(time.time())) + updated_at: int = field(default_factory=lambda: int(time.time())) + +ws_clients: set[WebSocket] = set() +main_loop: Optional[asyncio.AbstractEventLoop] = None +# conductor event queue: only user messages and subagent-done events enter here. +chat_messages: List[dict] = [] + +def now_ms() -> int: + return int(time.time() * 1000) + +def short_id() -> str: + return uuid.uuid4().hex[:8] + +_TURN_SPLIT_RE = re.compile(r'\**LLM Running \(Turn \d+\) \.\.\.\**') +_SUMMARY_RE = re.compile(r'(.*?)\s*', re.DOTALL) + +def extract_last_summary(full: str) -> str: + """Extract the latest content for in-progress display.""" + matches = _SUMMARY_RE.findall(full or "") + if not matches: return "" + s = matches[-1].strip() + return s[-1000:] if len(s) > 1000 else s + +def extract_last_text_reply(full: str) -> str: + """Extract only the last turn's text reply (like stapp.py fold_turns logic).""" + # Split by turn markers, take last segment + parts = _TURN_SPLIT_RE.split(full) + last = parts[-1] if parts else full + # Strip tags + last = _SUMMARY_RE.sub('', last) + # Strip [Status] and [Info] lines + last = re.sub(r'\[(Status|Info)\][^\n]*\n?', '', last) + # Strip trailing whitespace + last = last.strip() + # Cap length + return last[-3000:] if len(last) > 3000 else last + +def clean_log_text(s: str) -> str: + if not s: return s + s = re.sub(r'`{5}\n.*?`{5}\n?', '', s, flags=re.DOTALL) + s = re.sub(r'🛠️ Tool: `([^`]+)`\s*📥 args:\n`{4}.*?`{4}\n?', r'🛠️ `\1`\n', s, flags=re.DOTALL) + s = re.sub(r'^🛠️ .*\n?', '', s, flags=re.MULTILINE) # remove tool call summary lines + s = re.sub(r'.*?\s*', '', s, flags=re.DOTALL) + s = re.sub(r'^\s*\[(?:Info|Status)\][^\n]*\n?', '', s, flags=re.MULTILINE) + s = re.sub(r'^\s*`{4,5}\s*$\n?', '', s, flags=re.MULTILINE) + s = re.sub(r'\n{3,}', '\n\n', s) + return s.strip() + +def schedule_broadcast(payload: dict): + if main_loop and main_loop.is_running(): + asyncio.run_coroutine_threadsafe(broadcast(payload), main_loop) + +async def broadcast(payload: dict): + dead = [] + for ws in list(ws_clients): + try: await ws.send_json(payload) + except Exception: dead.append(ws) + for ws in dead: ws_clients.discard(ws) + +def push_cards(): schedule_broadcast({"type": "subagents", "items": pool.snapshot()}) + +def add_chat(msg: str, role: str = "conductor", files: list = None, images: list = None): + item = {"id": short_id(), "role": role, "msg": msg, "ts": now_ms(), "read": role != "user", "files": files or [], "images": images or []} + chat_messages.append(item) + if len(chat_messages) > 200: del chat_messages[:-200] + schedule_broadcast({"type": "chat", "item": item}) + return item + +def start_agent_runner(agent: GenericAgent, name: str): + t = threading.Thread(target=agent.run, name=name, daemon=True) + t.start(); return t + +def monitor_display_queue(agent_id: str, dq: "queue.Queue", trigger_when_done: bool): + acc = "" + while True: + item = dq.get() + if "next" in item: + chunk = item.get("next") or "" + acc += chunk + pool.on_display(agent_id, acc, done=False) + push_cards() + if "done" in item: + done = item.get("done") or acc + pool.on_display(agent_id, done, done=True) + push_cards() + if trigger_when_done: conductor.notify({"type": "subagent_done", "id": agent_id, "reply": done}) + break + + +class SubagentPool: + def __init__(self): + self.subagents: Dict[str, SubAgentState] = {} + self.lock = threading.RLock() + threading.Thread(target=self._auto_cleanup_loop, name="subagent-cleanup", daemon=True).start() + def snapshot(self) -> list[dict]: + with self.lock: + return [ + { + "id": s.id, + "prompt": s.prompt, + "reply": (extract_last_summary(s.reply) if s.status == "running" else extract_last_text_reply(s.reply)) if s.reply else "", + "status": s.status, + "created_at": s.created_at, + "updated_at": s.updated_at, + } + for s in self.subagents.values() + ] + def get(self, sid: str) -> Optional[SubAgentState]: + with self.lock: return self.subagents.get(sid) + def counts(self) -> tuple: + with self.lock: + running = sum(1 for s in self.subagents.values() if s.status == "running") + stopped = sum(1 for s in self.subagents.values() if s.status != "running") + return running, stopped + def on_display(self, agent_id: str, acc: str, done: bool): + with self.lock: + s = self.subagents.get(agent_id) + if s: + s.reply = acc + s.updated_at = int(time.time()) + s.status = "stopped" if done else "running" + def _auto_cleanup_loop(self): + IDLE_TIMEOUT = 3600 + while True: + time.sleep(300) + now = time.time() + to_abort = [] + with self.lock: + for sid, s in self.subagents.items(): + if s.status == "stopped" and (now - s.updated_at) > IDLE_TIMEOUT: to_abort.append((sid, s)) + for sid, s in to_abort: + s.agent.abort() + s.agent.task_queue.put("EXIT") + with self.lock: self.subagents.pop(sid, None) + if to_abort: push_cards() + def start_subagent(self, prompt: str, llm: Any = None) -> dict: + sid = short_id() + agent = GenericAgent() + agent.inc_out = True + agent.verbose = False + agent.no_print = True + if not _select_llm(agent, llm): _apply_desktop_model(agent) + th = start_agent_runner(agent, f"subagent-{sid}") + state = SubAgentState(id=sid, agent=agent, prompt=prompt, status="running", thread=th) + with self.lock: self.subagents[sid] = state + return self._send_msg(sid, prompt) + def _send_msg(self, sid, msg): + with self.lock: s = self.subagents.get(sid) + if not s: return {"error": "subagent not found", "id": sid} + dq = s.agent.put_task(msg, source=f"subagent:{sid}") + threading.Thread(target=monitor_display_queue, args=(sid, dq, True), name=f"monitor-{sid}", daemon=True).start() + push_cards() + return {"id": sid, "status": "running"} + def input_subagent(self, sid: str, msg: str, llm: Any = None) -> dict: + with self.lock: s = self.subagents.get(sid) + if not s: return {"error": "subagent not found", "id": sid} + if s.status == "running": return {"error": "subagent is still running, cannot input/reply. Start a new subagent instead.", "id": sid} + _select_llm(s.agent, llm) + s.prompt = msg + s.reply = "" + s.status = "running" + s.updated_at = int(time.time()) + return self._send_msg(sid, msg) + def keyinfo_subagent(self, sid: str, msg: str) -> dict: + with self.lock: s = self.subagents.get(sid) + if not s: return {"error": "subagent not found", "id": sid} + h = s.agent.handler + h.working['key_info'] = h.working.get('key_info', '') + f"\n[MASTER] {msg}" + s.updated_at = int(time.time()) + return {"id": sid, "status": "keyinfo_injected"} + +pool = SubagentPool() + +READMES = { +"api": f"""\ +Conductor API\tBase: http://{HOST}:{PORT} + +POST /chat\tbody: {{"msg": "..."}}\t给用户发消息 +POST /subagent\tbody: {{"prompt": "..."}}\t启动新subagent,返回 {{"id": "xxx"}};指定模型加参数llm(数字/名称) +POST /approval\tbody: {{"prompt": "...", "source": "..."}}\t推一条待批任务到前端(后端不存),用户同意则直接派发为subagent +POST /subagent/{{id}}\tbody: {{"action": "keyinfo", "msg": "..."}}\t注入key_info(agent下轮可见) +POST /subagent/{{id}}\tbody: {{"action": "input", "msg": "..."}}\t开新一轮任务;指定模型加参数llm(数字/名称) +POST /subagent/{{id}}\tbody: {{"action": "stop"}}\t中断执行但保留(可继续input/reply) +GET /chat?last=N\t返回最近N条对话(默认20) +GET /subagent\t返回 {{"items": [...]}}\t查看所有subagent状态 +GET /subagent/{{id}}?max_len=N\t返回单个subagent详情,reply经清洗后截取尾部max_len字(默认5000)。仅在摘要不够判断时使用 +""", +"usermsg": """\ +用户消息流程: +1. 结合记忆、上下文和用户偏好判断真实需求;不清楚/不能代劳时,用精简checklist一次性问用户。 +2. 判断是新任务还是延续现有任务;优先复用已有stopped subagent(用input追加),只有确实无关的新任务才新建。 +3. 分派前必须POST /chat告知用户:改写后的prompt + 分派方案(新建/复用哪个subagent)。 +4. 执行分派,完成即停。危险操作(改源码/删数据/安全敏感)必须改成先让subagent出方案;你验收后POST /chat请用户确认,确认后才继续执行。""", +"subagent": """\ +subagent完成流程: +1. 如果是IM采集subagent,按GET /readme/im进行而非本流程 +2. 读subagent输出;若最后一条不足以判断,GET /subagent/{id}?max_len=3000 补足信息。 +3. 预测用户是否满意;不满意就reply/keyinfo要求返工、修改、优化,继续监督,不急着报告。 +4. 预计用户满意后,POST /chat给简洁交付报告。""", +"im": """\ +你要审查IM采集subagent的输出,把**值得用户关注的内容**报告给用户或转化成"可点击执行"的待批TODO(approval)。 +先读L2记忆中User相关,推荐的动作和措辞要符合用户画像。 +要求: +1. 不要只凭采集摘要;重要事实要核实,需要判断时先派subagent补做必要调查,再下结论。 +2. 没有值得用户点击执行的动作就直接结束,不要打扰;尤其不要对执行回执/完成确认/纯闲聊报"无需关注"。 +3. 判断标准:私聊默认重要,群聊除非@用户否则忽略。 +4. 只有真正需要用户的内容才报告或形成TODO。不要推"去看看/研究一下"这种半成品。TODO必须是最后一步可直接执行的动作(发某段微信回复、回复某封邮件草稿、处理某PR、整理某文件等)。 +5. 如果形成用户TODO,POST /approval 推送,prompt里同时写清两部分: + ① 奏折式报告给用户拍板:背景(什么事/来自谁) + 已核实(你做了哪些调查/关键事实) + 判断(为什么这样建议) + 风险。用户看完这段就能直接拍板,不用再去翻原消息。 + ② 用户同意后该执行的完整任务指令(approval通过会直接作为subagent的prompt派发,必须具体到可直接执行)。""", +} + +class Conductor: + LOG_MAX = 50 + + def __init__(self): + self.inbox: "queue.Queue[dict]" = queue.Queue() # 收件箱:唯一对外接口 + self.agent: Optional[GenericAgent] = None + self.started = False + self.log: list = [] + self._model_lock = threading.RLock() + self._model_state = { + "configured": None, + "effective": None, + "fallbackReason": "no_models", + "current": None, + "running": False, + } + + def model_snapshot(self) -> dict: + with self._model_lock: + return dict(self._model_state) + + def _publish_model_state(self, state: dict, running: bool) -> None: + snapshot = {**state, "running": bool(running)} + with self._model_lock: + self._model_state = snapshot + schedule_broadcast({"type": "model", "model": snapshot}) + + def notify(self, event: dict): self.inbox.put(event) + + def _build_prompt(self, events: list) -> str: + running, stopped = pool.counts() + unread = sum(1 for m in chat_messages if m.get("role") == "user" and not m.get("read")) + done_count = sum(1 for e in events if e.get("type") == "subagent_done") + event_type = events[0].get("type") if events else "wake"; im_sources = [e.get("source") for e in events if e.get("type") == "im_signal"] + if event_type == "user_message": summary = f"[用户消息] {unread}条未读用户消息,GET /chat 读取;按GET /readme/usermsg处理。" + elif event_type == "subagent_done": summary = f"[subagent完成] {done_count}个完成报告;GET /subagent 查看并验收;IM subagent完成报告按GET /readme/im处理,其他subagent完成报告按GET /readme/subagent处理。" + elif event_type == "im_signal": summary = f"[IM信号] {', '.join(im_sources)} 有新消息;" + ";".join(f"GET /im_prompt/{s}取采集prompt" for s in im_sources) + ";尽量复用已有subagent。" + else: summary = f"[唤醒] subagents: {running} running, {stopped} stopped | {unread}条用户未读消息, {done_count}个subagent完成报告" + base = f"http://{HOST}:{PORT}" + return f"""你是agent总管。用户只和你对话,你负责调度、验收、交付,目标是降低用户管理多个agent的负担。 +API: {base};requests,GET /readme查用法,GET /chat读未读对话,GET /subagent看状态;POST /chat是唯一对用户说话方式。 + +铁律: +- 绝不亲自执行任务/探测环境;一切执行交给subagent。你只分析、派遣、审查、沟通。 +- 每次唤醒只做最小必要动作(发消息/开subagent/reply/keyinfo/abort),做完立刻停,等待下次事件唤醒。 +- 改写prompt时严禁添加用户未提及的假设、工具、前提条件。只能精炼/结构化用户原意,不能脑补,只能做很小的改写 + +原则: +- 信任subagent足够聪明,不要写具体步骤和容易探测的信息;能自己判断的自己判断,只在真正需要用户决策时打扰。\n +需要处理: +{summary}""" + + def _drain(self, dq: "queue.Queue", events: list) -> str: + event_label = ",".join(e.get("type", "") for e in events) or "wake" + cur_turn = None; buf = "" + + def flush(): + nonlocal buf + cleaned = clean_log_text(buf) + if cleaned: + item = {"id": short_id(), "ts": now_ms(), "event": event_label, + "turn": cur_turn, "text": cleaned} + self.log.append(item) + if len(self.log) > self.LOG_MAX: self.log.pop(0) + schedule_broadcast({"type": "log", "item": item}) + buf = "" + + while True: + item = dq.get() + if "next" in item: + t = item.get("turn") + if cur_turn is None: cur_turn = t + elif t != cur_turn: + flush(); cur_turn = t + buf += item.get("next", "") or "" + elif "done" in item: + if cur_turn is None: cur_turn = item.get("turn") + flush() + print("Conductor task done") + return + + def _run(self): + self.agent = GenericAgent() + self.agent.inc_out = True + start_agent_runner(self.agent, "conductor-agent") + self.started = True + while True: + # Block until first event arrives + first = self.inbox.get() + self.inbox.task_done() + # Short debounce: collect any additional events that arrived meanwhile + time.sleep(0.3) + events = [first] + while not self.inbox.empty(): + try: + events.append(self.inbox.get_nowait()) + self.inbox.task_done() + except Exception: + break + try: + prompt = self._build_prompt(events) + # Follow the desktop-selected model live: re-read before each task + # so switching models in the UI takes effect without restarting. + model_state = _apply_desktop_model(self.agent) + self._publish_model_state(model_state, running=True) + try: + dq = self.agent.put_task(prompt, source="conductor") + self._drain(dq, events) + finally: + self._publish_model_state(model_state, running=False) + except Exception as e: print(f"Conductor error: {e}") + + def start(self): threading.Thread(target=self._run, name="conductor-loop", daemon=True).start() + + +conductor = Conductor() + +# ---- IM poller: 探测conductor_im_plugins/下各插件,信号变化→唤醒总管 ---- +IM_DIR, IM_COOLDOWN = os.path.join(os.path.dirname(__file__), "conductor_im_plugins"), 300 +IM_PROMPTS: Dict[str, str] = {} # source -> 采集prompt(派采集subagent时按需取) + +def im_poll_loop(): + import importlib.util + mods, last_fire = {}, {} + for f in (x for x in os.listdir(IM_DIR) if x.endswith(".py") and not x.startswith("_")): + spec = importlib.util.spec_from_file_location(f[:-3], os.path.join(IM_DIR, f)) + m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m) + if hasattr(m, "check"): + mods[f[:-3]] = m + IM_PROMPTS[f[:-3]] = getattr(m, "PROMPT", "") + last_check = {} + while True: + time.sleep(10) + for name, m in mods.items(): + now = time.time() + if now - last_check.get(name, 0) < getattr(m, "INTERVAL", 30): continue + last_check[name] = now + try: + if not m.check() or now - last_fire.get(name, 0) < IM_COOLDOWN: continue + except Exception: continue + last_fire[name] = now + conductor.notify({"type": "im_signal", "source": name}) + +@app.get("/token-stats") +def conductor_token_stats(): + import cost_tracker + return {"records": [{"thread": k, "input": v.input, "output": v.output, "cacheCreate": v.cache_create, "cacheRead": v.cache_read} for k, v in cost_tracker.all_trackers().items()]} + +@app.get("/") +def index(): return FileResponse(HTML_PATH) + +@app.get("/readme") +def readme(): return PlainTextResponse(READMES["api"]) + +@app.get("/readme/{topic}") +def readme_topic(topic: str): + if topic not in READMES: + return PlainTextResponse(f"Unknown topic: {topic}. Available: {', '.join(READMES.keys())}", status_code=404) + return PlainTextResponse(READMES[topic]) + +@app.get("/im_prompt/{source}") +def im_prompt(source: str): + if source not in IM_PROMPTS: + return PlainTextResponse(f"Unknown source: {source}. Available: {', '.join(IM_PROMPTS.keys())}", status_code=404) + return PlainTextResponse(IM_PROMPTS[source]) + +@app.get("/subagent") +def list_subagents(): return {"items": pool.snapshot()} + +@app.get("/subagent/{sid}") +def get_subagent(sid: str, max_len: int = 5000): + s = pool.get(sid) + if not s: + return JSONResponse({"error": "not found"}, status_code=404) + cleaned = clean_log_text(s.reply or "") + return {"id": s.id, "prompt": s.prompt, "status": s.status, + "reply": cleaned[-max_len:] if len(cleaned) > max_len else cleaned, + "created_at": s.created_at, "updated_at": s.updated_at} + +INSTR_DISPATCHED = "Task received. I'll handle THIS TASK from here. You MUST to do other task or end your reply." + +@app.post("/subagent") +def api_start_subagent(body: StartSubagentIn): + result = pool.start_subagent(body.prompt, body.llm) + result["instruction"] = INSTR_DISPATCHED + return result + +@app.post("/subagent/{sid}") +def api_subagent_action(sid: str, body: SubagentActionIn): + s = pool.get(sid) + if not s: return JSONResponse({"error": "subagent not found", "id": sid}, status_code=404) + action = body.action.lower().strip() + if action == "keyinfo": + result = pool.keyinfo_subagent(sid, body.msg) + result["instruction"] = "Received. I'll incorporate this. You MUST to do other task or end your reply." + return result + if action in ("input", "reply", "append", "message", "msg"): + result = pool.input_subagent(sid, body.msg, body.llm) + result["instruction"] = INSTR_DISPATCHED + return result + if action in ("abort", "stop"): + s.agent.abort() + s.status = "stopped" + s.updated_at = int(time.time()) + push_cards() + return {"id": sid, "status": "stopped"} + return JSONResponse({"error": f"unknown action: {body.action}"}, status_code=400) + +@app.get("/chat") +def api_get_chat(last: int = 20): + items = [m.copy() for m in chat_messages[-last:]] + for m in chat_messages: + if m.get("role") == "user" and not m.get("read"): m["read"] = True + schedule_broadcast({"type": "chat_read"}) + return {"items": items} + +@app.post("/chat") +def api_chat(body: ChatIn): + return add_chat(body.msg, role=body.role) + +@app.post("/approval") +def api_approval(body: ApprovalIn): + schedule_broadcast({"type": "approval", "item": {"id": short_id(), "prompt": body.prompt, "source": body.source}}) + return {"ok": True} + +@app.websocket("/ws") +async def websocket(ws: WebSocket): + await ws.accept() + ws_clients.add(ws) + try: + running = any(s.status == "running" for s in pool.subagents.values()) + await ws.send_json({"type": "hello", "subagents": pool.snapshot(), "chat": chat_messages, + "log": conductor.log, "running": running, + "model": conductor.model_snapshot()}) + while True: + data = await ws.receive_json() + msg = (data.get("msg") or "").strip() + if not msg: continue + add_chat(msg, role="user", files=data.get("files") or [], images=data.get("images") or []) + conductor.notify({"type": "user_message", "msg": msg}) + except WebSocketDisconnect: pass + finally: ws_clients.discard(ws) + +if __name__ == "__main__": + import uvicorn + # bridge 自启 conductor 时传 --no-browser:不在用户浏览器里弹一个独立 conductor UI, + # 用户从桌面版「指挥家」页直接连过来即可。手动跑 conductor.py(没带 flag)保持原行为。 + if "--no-browser" not in sys.argv: + import webbrowser, threading + threading.Timer(1.0, lambda: webbrowser.open(f"http://{HOST}:{PORT}")).start() + uvicorn.run("conductor:app", host=HOST, port=PORT, reload=False) diff --git a/frontends/conductor_im_plugins/_TEMPLATE.py b/frontends/conductor_im_plugins/_TEMPLATE.py new file mode 100644 index 000000000..a1dc55245 --- /dev/null +++ b/frontends/conductor_im_plugins/_TEMPLATE.py @@ -0,0 +1,19 @@ +"""新IM插件模板:复制为 yourim.py(_开头的不加载),填好下面三件套即可被自动发现。 + +契约(与 wechat.py / email.py 同构): + INTERVAL : check() 的轮询间隔(秒) + PROMPT : 派给采集subagent的完整指令。它是有记忆/工具的完整GA, + 所以只需给三样:用什么工具看新消息(指向SOP)、按什么标准过滤、汇报边界。 + 注意限量:让它用"天然有界"的工具(如最近N条),禁止开放式全量扫描。 + check() : 现在有新东西吗?必须廉价(毫秒级,无LLM)。框架另有冷却,宁可误报。 +""" + +INTERVAL = 60 + +PROMPT = """\ +你是XX采集subagent。先读记忆中用户画像,再用<工具/SOP名>查看最近消息,过滤后汇报值得关注项并补全上下文。 +不执行外部动作;无值得关注的就一句话说明。""" + + +def check() -> bool: + return False diff --git a/frontends/conductor_im_plugins/_email_example.py b/frontends/conductor_im_plugins/_email_example.py new file mode 100644 index 000000000..b9b43b156 --- /dev/null +++ b/frontends/conductor_im_plugins/_email_example.py @@ -0,0 +1,15 @@ +"""邮件:存在未读邮件 → 有新东西。""" + +INTERVAL = 7200 + +PROMPT = """\ +你是邮件采集subagent。先读记忆中用户画像,再用 ezgmail 检查未读邮件,过滤后汇报值得关注项并补全上下文(发件人身份/线程/附件摘要)。 +过滤营销和自动通知,不确定的标"低优先级观察"。不回复不执行外部动作;无值得关注的就一句话说明。""" + + +def check() -> bool: + try: + import ezgmail + return bool(ezgmail.unread()) + except Exception: + return False diff --git a/frontends/conductor_im_plugins/_lark_example.py b/frontends/conductor_im_plugins/_lark_example.py new file mode 100644 index 000000000..7cb7771b4 --- /dev/null +++ b/frontends/conductor_im_plugins/_lark_example.py @@ -0,0 +1,27 @@ +"""飞书:lark-cli 轮询 config.local.json {"lark_chat_ids": [...]} 中的会话,INTERVAL 内有新消息 → 触发。""" +import glob, json, os, subprocess, time + +INTERVAL = 300 + +PROMPT = """\ +你是飞书采集subagent。先读记忆中用户画像,再用 lark-cli 查看最近消息(详见 lark_cli_sop;会话ID取本插件目录 config.local.json 的 lark_chat_ids,逐个 `lark-cli im +chat-messages-list --as user --chat-id --sort desc --page-size 10`),挑出刚出现的新消息,过滤后汇报值得关注项并补全上下文。 +不执行外部动作;无值得关注的就一句话说明。""" + +_CFG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.local.json") +_NODE = glob.glob(os.path.expandvars(r"%APPDATA%\fnm\node-versions\*\installation")) # lark-cli在fnm node下 + + +def check() -> bool: + cfg = json.load(open(_CFG, encoding="utf-8")) if os.path.exists(_CFG) else {} + env = {**os.environ, "PATH": os.pathsep.join(_NODE + [os.environ.get("PATH", "")])} + start = int(time.time()) - INTERVAL - 5 + for cid in cfg.get("lark_chat_ids", []): + r = subprocess.run( + f"lark-cli im +chat-messages-list --as user --chat-id {cid} --start {start} --page-size 1", + shell=True, capture_output=True, text=True, encoding="utf-8", errors="replace", env=env) + try: + if json.loads(r.stdout)["data"]["total"]: + return True + except Exception: + pass + return False diff --git a/frontends/continue_cmd.py b/frontends/continue_cmd.py new file mode 100644 index 000000000..758898060 --- /dev/null +++ b/frontends/continue_cmd.py @@ -0,0 +1,1181 @@ +"""`/continue` command: list & restore past model_responses sessions. +Pure functions + one `install(cls)` monkey-patch entry. No side effects at import. +""" +import ast, atexit, glob, json, os, random, re, shutil, threading, time +_LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + 'temp', 'model_responses') +_LOG_GLOB = os.path.join(_LOG_DIR, 'model_responses_*.txt') +_BLOCK_RE = re.compile(r'^=== (Prompt|Response) ===.*?\n(.*?)(?=^=== (?:Prompt|Response) ===|\Z)', + re.DOTALL | re.MULTILINE) +_SUMMARY_RE = re.compile(r'\s*(.*?)\s*', re.DOTALL) +_ROUND_HEADER_RE = re.compile(rb'^=== (Prompt|Response) ===', re.MULTILINE) +_ROUNDS_CACHE_PATH = os.path.join(os.path.expanduser('~'), '.genericagent', 'continue_rounds_cache.json') +_ROUNDS_CACHE_VERSION = 1 +_rounds_cache = None +_rounds_cache_dirty = False + +def _rel_time(mtime): + d = int(time.time() - mtime) + if d < 60: return f'{d}秒前' + if d < 3600: return f'{d // 60}分前' + if d < 86400: return f'{d // 3600}小时前' + return f'{d // 86400}天前' + +def _pairs(content): + blocks, pairs, pending = _BLOCK_RE.findall(content or ''), [], None + for label, body in blocks: + if label == 'Prompt': pending = body.strip() + elif pending is not None: + pairs.append((pending, body.strip())); pending = None + return pairs + +def _first_user(pairs): + for p, _ in pairs: + try: msg = json.loads(p) + except Exception: continue + if not isinstance(msg, dict): continue + for blk in msg.get('content', []) or []: + if isinstance(blk, dict) and blk.get('type') == 'text': + t = strip_project_mode(blk.get('text') or '').strip() + if t and '' not in t and not t.startswith('### [WORKING MEMORY]'): + return t + for p, _ in pairs[:1]: + for line in p.splitlines(): + s = line.strip() + if s and not s.startswith('###'): return s + return '' + + +def _last_user(text): + """Last real user prompt. Scans `=== Prompt ===` blocks directly (no + Prompt/Response pairing, so response-less/aborted sessions still preview), + newest-first, returning the first one `_user_text` accepts (it drops + tool_result continuations + all _INJECT_MARKERS). Better preview anchor than + the first prompt — reflects what the session was most recently about.""" + for label, body in reversed(_BLOCK_RE.findall(text or '')): + if label == 'Prompt': + t = _user_text(body) + if t: + return t + return '' + + +def _last_summary(pairs): + for _, response_body in reversed(pairs): + try: + blocks = ast.literal_eval(response_body) + except Exception: + continue + if not isinstance(blocks, list): + continue + text_parts = [] + for block in blocks: + if isinstance(block, dict) and block.get('type') == 'text': + text = block.get('text', '') + if isinstance(text, str) and text: + text_parts.append(text) + match = _SUMMARY_RE.search('\n'.join(text_parts)) + if match: + summary = match.group(1).strip() + if summary: + return summary + return '' + + +def _preview_text(pairs): + return _last_summary(pairs) or _first_user(pairs) + +def _recent_context(my_pid, n=5): + """扫描最近 n 个 model_response 文件(排除自身),提取 lastQ / lastA。""" + out = [] + for f in sorted(glob.glob(_LOG_GLOB), key=os.path.getmtime, reverse=True): + m = re.search(r'model_responses_(\d+)', os.path.basename(f)) + if not m or m.group(1) == str(my_pid): continue + try: c = open(f, encoding='utf-8', errors='ignore').read() + except Exception: continue + q = s = "" + for hm in re.finditer(r'(.*?)', c, re.DOTALL): + u = re.search(r'\[USER\]:\s*(.+?)(?:\\n|<)', hm.group(1)) + if u: q = u.group(1) + sm = _SUMMARY_RE.search(c) + if sm: s = sm.group(1).strip() + q, s = q[:60].strip(), s[:60].replace('\n', ' ').strip() + out.append(f'· {m.group(1)} | lastQ: {q or "-"} | lastA: {s or "-"}') + if len(out) >= n: break + return ('[RecentContext] 近期并行会话(非当前):\n' + '\n'.join(out) + '\n[/RecentContext]') if out else "" + +def _parse_native_history(pairs): + history = [] + for p, r in pairs: + try: user_msg = json.loads(p) + except Exception: return None + try: blocks = ast.literal_eval(r) + except Exception: return None + if not (isinstance(user_msg, dict) and user_msg.get('role') == 'user'): return None + if not isinstance(blocks, list): return None + history.append(user_msg) # runtime history 尊重日志真实 prompt(含 project-mode 当轮注入) + history.append({'role': 'assistant', 'content': blocks}) + return history + + +def parse_native_log(path, allow_empty=False): + """Parse a native `model_responses_*.txt` log into backend.history. + + Public wrapper around the mature `/continue` parser. It intentionally only + restores complete Prompt→Response pairs; dangling Prompt / partial turns keep + the current continue semantics and are ignored. Returns: + - list (possibly [] when allow_empty=True) on native success; + - None when the file is unreadable / non-native / empty without allow_empty. + """ + try: + with open(path, encoding='utf-8', errors='replace') as fh: + content = fh.read() + except Exception: + return [] if allow_empty else None + pairs = _pairs(content) + if not pairs: + # Native-looking but incomplete logs (e.g. dangling Prompt without Response) + # have block headers but no complete pairs. For worldline's allow_empty mode + # this means "0 completed rounds", not "parse failed → trust live history". + if allow_empty and (_is_empty_log(path) or _BLOCK_RE.findall(content or '')): + return [] + return None + return _parse_native_history(pairs) + + +def _derive_hist_info(history): + """从 native history 重建 history_info(轮级纪要):真实用户提问 → `[USER]: …`;每条 + assistant(=一轮) → `[Agent] `(无 summary 取首行)。与 ga.turn_end_callback / + worldline 树同口径。纯函数、不依赖 worldline,供续接 opt-in 恢复工作记忆(见 restore_wm)。""" + def _all_text(m): + c = m.get('content') if isinstance(m, dict) else None + if isinstance(c, str): return c + if isinstance(c, list): + return '\n'.join(b.get('text', '') for b in c + if isinstance(b, dict) and b.get('type') == 'text') + return '' + def _is_tool_result(m): + c = m.get('content') if isinstance(m, dict) else None + return isinstance(c, list) and any( + isinstance(b, dict) and b.get('type') == 'tool_result' for b in c) + out = [] + for m in history or []: + if not isinstance(m, dict): continue + role = m.get('role') + if role == 'user': + if _is_tool_result(m): continue # tool_result 轮不是提问 + t = strip_project_mode(_all_text(m)).strip() + if t and not t.startswith(_INJECT_MARKERS): + out.append(f'[USER]: {t}') + elif role == 'assistant': + txt = re.sub(r'```.*?```|.*?', '', _all_text(m), flags=re.DOTALL) + mt = re.search(r'(.*?)', txt, re.DOTALL) + s = mt.group(1).strip()[:80] if (mt and mt.group(1).strip()) else '' + if not s: + s = next((ln.strip()[:80] for ln in txt.splitlines() if ln.strip()), '(无摘要)') + out.append(f'[Agent] {s}') + return out + + +_PREVIEW_WIN = 32 * 1024 + +# Content-grep budget for `/continue` search box: read at most this many bytes +# per session (head window) so 17MB files don't stall the UI. Empirically the +# user-typed prompt + first model reply + early summaries live in the first MB, +# which is what users actually want to recall sessions by. +_GREP_WIN = 1 * 1024 * 1024 + + +def file_contains_all(path, terms, max_bytes=_GREP_WIN): + """True iff every lowercase term in `terms` appears in the first + `max_bytes` of `path` (case-insensitive). Empty `terms` returns True so + callers can short-circuit. Reads as bytes + .lower() to avoid utf-8 cost + and stays within a fixed memory envelope regardless of file size. + """ + if not terms: + return True + try: + with open(path, 'rb') as fh: + buf = fh.read(max_bytes) + except OSError: + return False + if not buf: + return False + hay = buf.lower() + for t in terms: + if t and t.encode('utf-8', errors='ignore') not in hay: + return False + return True + + +def search_sessions(query, sessions, max_bytes=_GREP_WIN): + """Filter `sessions` ([(path, mtime, preview, n), ...]) by content grep. + + `query` is whitespace-split into AND terms (case-insensitive). Each + session is kept iff its path/preview already match OR the first + `max_bytes` of its file contain every term. Order is preserved. + Empty/whitespace query returns the list as-is. + """ + q = (query or '').strip().lower() + if not q: + return list(sessions or []) + terms = [t for t in q.split() if t] + if not terms: + return list(sessions or []) + out = [] + for item in sessions or []: + path = item[0] if len(item) > 0 else '' + preview = item[2] if len(item) > 2 else '' + meta = (os.path.basename(path) + '\n' + (preview or '')).lower() + if all(t in meta for t in terms): + out.append(item) + continue + if file_contains_all(path, terms, max_bytes=max_bytes): + out.append(item) + return out + + +def _preview_from_file(path): + """Cheap preview: last in tail window, else first user line in head window.""" + try: + sz = os.path.getsize(path) + with open(path, 'rb') as fh: + if sz <= _PREVIEW_WIN * 2: + head = tail = fh.read() + else: + head = fh.read(_PREVIEW_WIN) + fh.seek(-_PREVIEW_WIN, 2); tail = fh.read() + except OSError: return '' + tail_s = tail.decode('utf-8', errors='replace') + # Use only the latest , and reject it if dirty. Models sometimes emit + # an unclosed , so the non-greedy DOTALL match pairs it with a far-away + # and swallows === block headers / JSON across rounds. Treat such a + # match as invalid and fall through to the last user prompt (don't dig older ones). + cands = _SUMMARY_RE.findall(tail_s) + if cands: + s = ' '.join(cands[-1].split()) + if s and '=== ' not in s and '"role"' not in s and len(s) <= 200: + return s + # Summary invalid/absent -> last real user prompt (JSON-aware, skips anchors; + # scans Prompt blocks directly so response-less sessions still preview). + lu = _last_user(tail_s) or _last_user(head.decode('utf-8', errors='replace')) + if lu: + return ' '.join(lu.split())[:120] + return '' + + +def _rounds_cache_key(path): + return os.path.normcase(os.path.abspath(path)) + + +def _load_rounds_cache(): + """Load lazy mtime/size keyed round-count cache for /continue. + + Cache is intentionally triggered only by list_sessions(): no TUI startup cost, + no logging-path coupling. Missing/stale entries are recomputed on demand. + """ + global _rounds_cache + if _rounds_cache is not None: + return _rounds_cache + _rounds_cache = {} + try: + with open(_ROUNDS_CACHE_PATH, encoding='utf-8') as fh: + data = json.load(fh) + if isinstance(data, dict) and data.get('version') == _ROUNDS_CACHE_VERSION: + items = data.get('items') + if isinstance(items, dict): + _rounds_cache = items + except Exception: + _rounds_cache = {} + return _rounds_cache + + +def _save_rounds_cache(valid_keys=None): + global _rounds_cache_dirty + if not _rounds_cache_dirty or _rounds_cache is None: + return + try: + if valid_keys is not None: + keep = set(valid_keys) + for k in list(_rounds_cache.keys()): + if k not in keep: + _rounds_cache.pop(k, None) + os.makedirs(os.path.dirname(_ROUNDS_CACHE_PATH), exist_ok=True) + tmp = _ROUNDS_CACHE_PATH + '.tmp' + data = {'version': _ROUNDS_CACHE_VERSION, 'items': _rounds_cache} + with open(tmp, 'w', encoding='utf-8') as fh: + json.dump(data, fh, ensure_ascii=False, separators=(',', ':')) + os.replace(tmp, _ROUNDS_CACHE_PATH) + _rounds_cache_dirty = False + except Exception: + # Cache is a performance hint only; never break /continue on cache I/O. + pass + + +def _count_complete_rounds_from_file(path): + """Count completed Prompt→Response pairs using only block headers. + + Counting Prompt headers alone overcounts an in-flight/incomplete last round. + Header-pair counting matched `_pairs()` on sampled real logs while avoiding + expensive UTF-8 decode / body regex parsing. + """ + try: + with open(path, 'rb') as fh: + data = fh.read() + except OSError: + return 0 + pending = False + rounds = 0 + for m in _ROUND_HEADER_RE.finditer(data): + if m.group(1) == b'Prompt': + pending = True + elif pending: + rounds += 1 + pending = False + return rounds + + +def _rounds_for_file(path, st): + global _rounds_cache_dirty + cache = _load_rounds_cache() + key = _rounds_cache_key(path) + size = int(getattr(st, 'st_size', 0)) + mtime_ns = int(getattr(st, 'st_mtime_ns', int(getattr(st, 'st_mtime', 0) * 1_000_000_000))) + ent = cache.get(key) + if isinstance(ent, dict) and ent.get('size') == size and ent.get('mtime_ns') == mtime_ns: + try: + return int(ent.get('rounds', 0)), key + except Exception: + pass + n = _count_complete_rounds_from_file(path) + cache[key] = {'size': size, 'mtime_ns': mtime_ns, 'rounds': int(n)} + _rounds_cache_dirty = True + return n, key + + +def list_sessions(exclude_pid=None, exclude_log=None, rewind_root=None): + """Newest-first list of (path, mtime, preview_text, n_rounds). Preview uses head/tail window only. + + `exclude_log` (basename, e.g. 'model_responses_123456.txt') drops the caller's + OWN current session — preferred over `exclude_pid`, which assumed the log file + was named by PID (it isn't: agentmain mints a random 6-digit logid), so the + pid tag never matched and the current session leaked into its own list.""" + files = glob.glob(_LOG_GLOB) + if exclude_pid is not None: + tag = f'model_responses_{exclude_pid}.txt' + files = [f for f in files if not f.endswith(tag)] + if exclude_log: + files = [f for f in files if os.path.basename(f) != exclude_log] + out = [] + valid_keys = [] + for f in files: + try: + st = os.stat(f) + mtime, sz = st.st_mtime, st.st_size + except OSError: + continue + if sz < 32: + continue + preview = _preview_from_file(f) + if not preview: + continue + rounds, key = _rounds_for_file(f, st) + valid_keys.append(key) + out.append((f, mtime, preview, rounds)) + _save_rounds_cache(valid_keys) + # 【门控·worldline】树感知发现:日志存在但为空且有非空世界线树的会话(回退到起点后日志被 + # 清空 → 上面 sz<32 跳过了)。仅当调用方显式传 rewind_root 时启用 → 其他 UI 不传, + # 行为逐字节不变。只读 tree.json 的 nodes/head(不依赖 worldline 模块)。 + if rewind_root and os.path.isdir(rewind_root): + have = {os.path.basename(p) for p, *_ in out} + try: + keys = os.listdir(rewind_root) + except OSError: + keys = [] + for key in keys: + if not key.startswith('model_responses_'): + continue + log_name = key + '.txt' + if log_name in have or log_name == exclude_log: + continue + log_path = os.path.join(_LOG_DIR, log_name) + try: # 仅收"日志确实为空"的(非空日志已被主循环收录) + if os.path.getsize(log_path) >= 32: + continue + except OSError: + continue # 日志缺失(如已归档)不作为可续会话展示 + try: + with open(os.path.join(rewind_root, key, 'tree.json'), encoding='utf-8') as fh: + d = json.load(fh) + except Exception: + continue + nodes = d.get('nodes') or {} + real = [v for v in nodes.values() if v.get('kind') != 'origin'] + if not real: # 只有 origin 的空树 → 无内容,跳过 + continue + try: + mtime = os.path.getmtime(os.path.join(rewind_root, key, 'tree.json')) + except OSError: + mtime = 0 + head = d.get('head') + title = (nodes.get(head, {}).get('title') if head else '') or '(已回退至会话起点)' + out.append((log_path, mtime, f'[世界线] {title}', len(real))) + out.sort(key=lambda x: x[1], reverse=True) + return out +_MD_ESCAPE_RE = re.compile(r'([\\`*_\[\]])') +def _escape_md(s): return _MD_ESCAPE_RE.sub(r'\\\1', s) + + +def _agent_clients(agent): + clients = [] + for client in getattr(agent, 'llmclients', []) or []: + if client not in clients: + clients.append(client) + current = getattr(agent, 'llmclient', None) + if current is not None and current not in clients: + clients.insert(0, current) + return clients + + +def _replace_backend_history(agent, history): + backend = getattr(getattr(agent, 'llmclient', None), 'backend', None) + if backend is not None and hasattr(backend, 'history'): + backend.history = list(history or []) + + +def _current_log_path(pid=None): + pid = os.getpid() if pid is None else pid + return os.path.join(_LOG_DIR, f'model_responses_{pid}.txt') + + +def _snapshot_current_log(pid=None): + """Persist current PID log as a standalone recoverable snapshot, then clear it.""" + path = _current_log_path(pid) + if not os.path.isfile(path): + return None + try: + with open(path, encoding='utf-8', errors='replace') as fh: + content = fh.read() + except Exception: + return None + if not _pairs(content): + return None + os.makedirs(_LOG_DIR, exist_ok=True) + pid = os.getpid() if pid is None else pid + stamp = time.strftime('%Y%m%d_%H%M%S') + snapshot = os.path.join(_LOG_DIR, f'model_responses_snapshot_{pid}_{stamp}_{time.time_ns() % 1_000_000_000:09d}.txt') + with open(snapshot, 'w', encoding='utf-8', errors='replace') as fh: + fh.write(content) + with open(path, 'w', encoding='utf-8', errors='replace'): + pass + return snapshot + + +def reset_conversation(agent, message='🆕 已开启新对话,当前上下文已清空'): + """Abort current work and clear all known frontend-visible conversation state.""" + try: + agent.abort() + except Exception: + pass + _snapshot_current_log() + if hasattr(agent, 'history'): + agent.history = [] + for client in _agent_clients(agent): + backend = getattr(client, 'backend', None) + if backend is not None and hasattr(backend, 'history'): + backend.history = [] + if hasattr(client, 'last_tools'): + client.last_tools = '' + if hasattr(agent, 'handler'): + agent.handler = None + return message + +def format_list(sessions, limit=20): + if not sessions: return '❌ 没有可恢复的历史会话' + lines = ['**可恢复会话**(输入 `/continue N` 恢复第 N 个):', ''] + for i, (_, mtime, first, n) in enumerate(sessions[:limit], 1): + preview = _escape_md((first or '(无法预览)').replace('\n', ' ')[:60]) + lines.append(f'{i}. `{_rel_time(mtime)}` · **{n} 轮** · {preview}') + return '\n'.join(lines) + +def restore(agent, path): + """Restore session at path. Returns (msg, is_full).""" + try: + with open(path, encoding='utf-8', errors='replace') as fh: + content = fh.read() + except Exception as e: return f'❌ 读取失败: {e}', False + pairs = _pairs(content) + if not pairs: return f'❌ {os.path.basename(path)} 为空或格式不符', False + history = _parse_native_history(pairs) + name = os.path.basename(path) + if history is not None: + agent.abort() + _replace_backend_history(agent, history) + return f'✅ 已恢复 {len(pairs)} 轮完整对话({name})\n(已写入 backend.history,可直接继续)', True + from chatapp_common import _restore_native_history, _restore_text_pairs + summary = _restore_text_pairs(content) or _restore_native_history(content) + if not summary: return f'❌ {name} 无法解析(非 native 且无摘要可提取)', False + agent.abort() + agent.history.extend(summary) + n = sum(1 for l in summary if l.startswith('[USER]: ')) + return f'⚠️ 非 native 格式,已降级恢复 {n} 轮摘要({name})\n(请输入新问题继续)', False + +def handle(agent, query, display_queue): + """Dispatch /continue or /continue N. Returns None if consumed else original query.""" + s = (query or '').strip() + if s == '/continue': + display_queue.put({'done': format_list(list_sessions(exclude_pid=os.getpid())), 'source': 'system'}) + return None + m = re.match(r'/continue\s+(\d+)\s*$', s) + if m: + sessions = list_sessions(exclude_pid=os.getpid()) + idx = int(m.group(1)) - 1 + if not (0 <= idx < len(sessions)): + display_queue.put({'done': f'❌ 索引越界(有效范围 1-{len(sessions)})', 'source': 'system'}) + return None + reset_conversation(agent, message=None) + msg, _ = restore(agent, sessions[idx][0]) + display_queue.put({'done': msg, 'source': 'system'}) + return None + return query + + +_INJECT_MARKERS = ('### [WORKING MEMORY]', '[SYSTEM TIPS]', '[SYSTEM]', '[System]', + '[DANGER]', '### [总结提炼经验]', + 'Continue from where you left off') + +# project_mode 插件把 `\n\n---\n[PROJECT MODE: ]\n…\n---` 追加到当轮 user +# message(见 plugins/project_mode._build_injection)。runtime history 尊重日志真实 prompt, +# 但 UI 预览/显示与派生 history_info 需要只取用户原话。老日志的 WORKING MEMORY 里还 +# 可能嵌着已污染的 `[USER]: ... [PROJECT MODE] ... [Agent] ...`,所以剥离支持 text +# 内任意位置与被截断的单行 title 形态。不能加进 _INJECT_MARKERS——那会把整条用户原话一起丢弃。 +_PM_BLOCK_RE = re.compile(r"\s*-{3,}\s*\[PROJECT MODE:.*?(?:\n-{3,}\s*|$)", re.DOTALL) + + +def strip_project_mode(text: str) -> str: + """剔除文本中由 project_mode 插件追加的 L1 注入块。""" + return _PM_BLOCK_RE.sub("", text or "") + + +def _user_text(prompt_body): + """User-typed text from a prompt JSON; '' if this is an agent auto-continuation. + + A Prompt is auto-continue when *either* (a) it carries any tool_result block + (so it's the next round of an in-flight LLM call), or (b) its text blocks all + match known injection prefixes ([WORKING MEMORY], [SYSTEM TIPS], [System] + regenerate prompts, [DANGER] guards, etc.). Real first-prompts only contain + one plain text block with no injection markers. + """ + try: msg = json.loads(prompt_body) + except Exception: return '' + if not isinstance(msg, dict): return '' + blocks = msg.get('content', []) or [] + if any(isinstance(b, dict) and b.get('type') == 'tool_result' for b in blocks): + return '' + for blk in blocks: + if isinstance(blk, dict) and blk.get('type') == 'text': + t = strip_project_mode(blk.get('text') or '').strip() + if t and not any(mk in t for mk in _INJECT_MARKERS): return t + return '' + + +def _assistant_text(response_body): + """Joined plain text from a response blocks repr; '' on parse failure. + Used by /export to grab the model's prose only, without tool noise. + """ + try: blocks = ast.literal_eval(response_body) + except Exception: return '' + if not isinstance(blocks, list): return '' + return '\n'.join(b['text'] for b in blocks + if isinstance(b, dict) and b.get('type') == 'text' + and isinstance(b.get('text'), str) and b['text'].strip()) + + +def _format_tool_use(block): + """Match agent_loop.py:78 verbose tool-call header byte-for-byte. + + MUST use agent_loop's `get_pretty_json`, not a plain `json.dumps`: the + former rewrites a `script` arg's `"; "` into `";\\n "`, so for tools + carrying `script` (code_run, web_execute_js) a plain dumps produces a + *different* fence body. The TUI's write/read/code cards content-address + their captures by `hash(get_pretty_json(args))`; a mismatched fence here + means the hash misses and the card silently falls back to the raw block.""" + name = block.get('name', '?') + args = block.get('input', {}) + try: + from agent_loop import get_pretty_json + pretty = get_pretty_json(args) + except Exception: + try: pretty = json.dumps(args, indent=2, ensure_ascii=False).replace('\\n', '\n') + except Exception: pretty = str(args) + return f"🛠️ Tool: `{name}` 📥 args:\n````text\n{pretty}\n````\n" + + +def _format_tool_result(content): + """Match agent_loop.py:79-81 five-backtick fence around tool output.""" + if isinstance(content, list): + parts = [] + for b in content: + if isinstance(b, dict) and b.get('type') == 'text': + parts.append(b.get('text', '') or '') + elif isinstance(b, str): + parts.append(b) + body = '\n'.join(parts) + else: + body = '' if content is None else str(content) + return f"`````\n{body}\n`````\n" + + +def _tool_results_from_prompt(prompt_body): + """Return {tool_use_id: formatted_fence} from a Prompt JSON's content blocks.""" + try: msg = json.loads(prompt_body) + except Exception: return {} + if not isinstance(msg, dict): return {} + out = {} + for blk in msg.get('content', []) or []: + if isinstance(blk, dict) and blk.get('type') == 'tool_result': + tid = blk.get('tool_use_id') or '' + if tid: out[tid] = _format_tool_result(blk.get('content')) + return out + + +def _format_response_segment(response_body, tool_results): + """Rebuild one LLM call's transcript slice: text blocks + tool_use headers + + matching tool_result fences. Mirrors agent_loop verbose output so fold_turns + sees the same string shape as live mode. + """ + try: blocks = ast.literal_eval(response_body) + except Exception: return '' + if not isinstance(blocks, list): return '' + texts, tool_parts = [], [] + for b in blocks: + if not isinstance(b, dict): continue + t = b.get('type') + if t == 'text': + s = b.get('text', '') + if isinstance(s, str) and s.strip(): texts.append(s) + elif t == 'tool_use': + tool_parts.append(_format_tool_use(b)) + tid = b.get('id') or '' + if tid and tid in tool_results: tool_parts.append(tool_results[tid]) + return '\n\n'.join(p for p in ['\n\n'.join(texts), '\n'.join(tool_parts)] if p) + + +_PLAN_ENTRY_RE = re.compile(r'enter_plan_mode\(\s*[\'"]([^\'"]+plan\.md)[\'"]') + + +def find_plan_entry(path): + """Last `enter_plan_mode("…plan.md")` call in a model_responses log. + + Plan mode has exactly one entry point (plan_sop.md): a `code_run` tool call + whose inline_eval script invokes `handler.enter_plan_mode(...)`. That call + survives in the log as a structured `tool_use` block — unlike a plan path + merely *mentioned* in chat text, it cannot be produced by the user typing + a filename. Scanning these blocks is therefore the restore criterion for + the plan card; the last match wins so re-entered plans track the newest. + + Returns the plan.md path string as written in the script, or None. + """ + try: + with open(path, encoding='utf-8', errors='replace') as f: + content = f.read() + except Exception: + return None + last = None + for _prompt, response in _pairs(content): + try: + blocks = ast.literal_eval(response) + except Exception: + continue + if not isinstance(blocks, list): + continue + for b in blocks: + if not (isinstance(b, dict) and b.get('type') == 'tool_use' + and b.get('name') == 'code_run'): + continue + m = _PLAN_ENTRY_RE.search(str((b.get('input') or {}).get('script') or '')) + if m: + last = m.group(1) + return last + + +def iter_write_captures(path): + """Replay a log's file_write/file_patch/file_read calls into capture dicts + the TUI can feed to its card renderers (`_WRITE_CAP`), keyed later by + hash(get_pretty_json). + + Live mode fills `_WRITE_CAP` from tool_before/tool_after hooks (with a real + pre-write disk snapshot); on /continue that history is gone, but the + structured `tool_use.input` survives in the log — clean, complete args. We + also track each path's content *within this session* so a file + written/patched several times shows real old→new diffs (not N× full "new + file"). Files first touched by an untracked on-disk state still fall back + to a full-content block. + + Returns write entries `{"name", "args", "existed", "old", "status", "msg"}` + and read entries `{"name", "args", "content"}` in call order. `status`/`msg` + come from the matching tool_result so the header can show ✗ on a failed + write; a read's `content` is the raw tool_result text (the read card strips + its LLM-facing chrome itself). + """ + try: + with open(path, encoding='utf-8', errors='replace') as f: + content = f.read() + except Exception: + return [] + pairs = _pairs(content) + # tool_use_id -> (status, msg) from any prompt's tool_result blocks (the + # result lands in the *next* round's Prompt as a tool_result whose content + # is the json-dumped outcome.data, e.g. {"status":"success","msg":...}). + # tr_raw keeps the undecoded text — a file_read result is plain text. + tr_status, tr_raw = {}, {} + for prompt, _ in pairs: + try: + msg_obj = json.loads(prompt) + except Exception: + continue + if not isinstance(msg_obj, dict): + continue + for blk in msg_obj.get('content', []) or []: + if not (isinstance(blk, dict) and blk.get('type') == 'tool_result'): + continue + tid = blk.get('tool_use_id') + c = blk.get('content') + if isinstance(c, list): + c = ''.join(b.get('text', '') for b in c + if isinstance(b, dict) and b.get('type') == 'text') + if tid and isinstance(c, str): + tr_raw[tid] = c + try: + d = json.loads(c) if isinstance(c, str) else None + except Exception: + d = None + if tid and isinstance(d, dict): + tr_status[tid] = (d.get('status'), str(d.get('msg') or '')) + + out, state = [], {} + for _prompt, response in pairs: + try: + blocks = ast.literal_eval(response) + except Exception: + continue + if not isinstance(blocks, list): + continue + for b in blocks: + if not (isinstance(b, dict) and b.get('type') == 'tool_use'): + continue + name = b.get('name') + if name not in ('file_write', 'file_patch', 'file_read', 'code_run'): + continue + args = b.get('input') or {} + p = args.get('path') + if name == 'file_read': + out.append({'name': name, 'args': args, + 'content': tr_raw.get(b.get('id'))}) + continue + if name == 'code_run': + # data = the tool_result text; a dict result is JSON, an + # inline_eval / code-missing result is plain text. Pass the + # parsed dict when possible so the card reads exit_code/stdout; + # else the raw string (the card handles both). + raw = tr_raw.get(b.get('id')) + d = raw + try: + parsed = json.loads(raw) if isinstance(raw, str) else None + if isinstance(parsed, dict): + d = parsed + except Exception: + pass + out.append({'name': name, 'args': args, 'data': d}) + continue + st, mg = tr_status.get(b.get('id'), (None, '')) + if name == 'file_patch': + # If this file's content is tracked within the session, pass it as + # the pre-write full file so the renderer can do a whole-file diff + # (real line numbers + context); else fall back to the fragment. + pre = state.get(p, '') + out.append({'name': name, 'args': args, + 'existed': p in state, 'old': pre, + 'status': st, 'msg': mg}) + if st == 'error': + continue # failed call left the disk untouched — don't book it + old = args.get('old_content') or '' + if p in state and old: + state[p] = state[p].replace(old, args.get('new_content') or '', 1) + else: # file_write + existed = p in state + old = state.get(p, '') + new = str(args.get('content') or '') + mode = str(args.get('mode') or 'overwrite') + out.append({'name': name, 'args': args, 'existed': existed, 'old': old, + 'status': st, 'msg': mg}) + if st == 'error': + continue # failed call left the disk untouched — don't book it + if mode == 'append': + state[p] = old + new + elif mode == 'prepend': + state[p] = new + old + else: + state[p] = new + return out + + +def extract_ui_messages(path): + """Parse a model_responses log into [{role, content}, ...] for UI replay. + + Each user-initiated round becomes one user bubble plus one assistant bubble. + Auto-continuation LLM calls are concatenated into the same assistant bubble, + separated by ``**LLM Running (Turn N) ...**`` markers. Tool calls and their + results are rendered into the assistant content using the same string format + that agent_loop yields live, so fold_turns can fold them identically. + """ + try: + with open(path, encoding='utf-8', errors='replace') as f: content = f.read() + except Exception: return [] + pairs = _pairs(content) + if not pairs: return [] + # tool_results live in the *next* Prompt's content; index look-ahead. + next_tr = [{} for _ in pairs] + for i in range(len(pairs) - 1): + next_tr[i] = _tool_results_from_prompt(pairs[i + 1][0]) + + out, assistant, round_turn = [], None, 0 + for i, (prompt, response) in enumerate(pairs): + user = _user_text(prompt) + seg = _format_response_segment(response, next_tr[i]) + if user: + if assistant is not None: out.append(assistant) + out.append({'role': 'user', 'content': user}) + # Turn 1 marker too — agent_loop yields one per LLM call, including the + # first, so fold_turns treats every non-last call uniformly as a fold. + assistant = {'role': 'assistant', + 'content': f"\n\n**LLM Running (Turn 1) ...**\n\n{seg}"} + round_turn = 1 + else: + if assistant is None: + assistant = {'role': 'assistant', 'content': ''} + round_turn = 1 + round_turn += 1 + marker = f"\n\n**LLM Running (Turn {round_turn}) ...**\n\n" + assistant['content'] = (assistant['content'] or '') + marker + seg + if assistant is not None: out.append(assistant) + return [m for m in out if (m.get('content') or '').strip()] + + +def handle_frontend_command(agent, query, exclude_pid=None): + """Frontend-friendly /continue entry that returns text directly.""" + s = (query or '').strip() + exclude_pid = os.getpid() if exclude_pid is None else exclude_pid + if s == '/continue': + return format_list(list_sessions(exclude_pid=exclude_pid)) + m = re.match(r'/continue\s+(\d+)\s*$', s) + if not m: + return '用法: /continue 或 /continue N' + sessions = list_sessions(exclude_pid=exclude_pid) + idx = int(m.group(1)) - 1 + if not (0 <= idx < len(sessions)): + return f'❌ 索引越界(有效范围 1-{len(sessions)})' + reset_conversation(agent, message=None) + msg, _ = restore(agent, sessions[idx][0]) + return msg + + +# =========================================================================== +# 原地复原(in-place continue)共享层 —— 仅供 TUI(tui_v2/tui_v3 及其 rewind 副本) +# 调用;其它前端(IM/qt/streamlit…)不调用这些函数,行为完全不受影响。 +# +# 模型:每个会话 = 一个 `model_responses_.txt`,身份就是文件本身。 +# · 原地续 X = 把 agent 的 log_path 指回 X,之后的轮次追加到 X 本身(同一会话延续)。 +# · 拷贝续 X = 铸一个新 logid、把 X 拷进去,在副本上续;X 原件不动(并发安全)。 +# · 切走/新对话 = 释放当前锁、旧日志原样留作"空闲会话"(不存快照、不清空),新对话铸新 logid。 +# +# 独占:每个 TUI 会话出生即持有自己日志的一把锁(`.locks/.lock`); +# 整进程共用一个心跳线程,每 ~5s touch 锁文件 mtime(无 fsync)。 +# 判活 = 锁 mtime 在 30s 内新鲜;超 30s 视为持锁者已死,可被接管。 +# 抢锁用原子 O_EXCL;锁基础设施任何故障都降级为"假定空闲、放行续接",绝不阻断 /continue。 +# =========================================================================== + +_LOCK_DIR = os.path.join(_LOG_DIR, '.locks') +_HB_INTERVAL = 5.0 # 心跳间隔(秒) +_STALE_AFTER = 30.0 # 超过这么久无心跳 → 持锁者视为已死,可接管 +_held_locks = set() # 本进程当前持有的 log_path 集合 +_hb_lock = threading.Lock() +_hb_thread = None + + +def _lock_path(log_path): + base = os.path.splitext(os.path.basename(log_path))[0] + return os.path.join(_LOCK_DIR, base + '.lock') + + +def _read_lock(lock_file): + try: + with open(lock_file, encoding='utf-8') as fh: + return json.load(fh) + except Exception: + return None + + +def _lock_fresh(lock_file): + """心跳新鲜度 = 锁文件 mtime 距今 < _STALE_AFTER。""" + try: + return (time.time() - os.path.getmtime(lock_file)) < _STALE_AFTER + except OSError: + return False + + +def session_occupant(log_path): + """若 `log_path` 正被一个活着的(心跳新鲜)进程持有,返回其 owner 元数据 dict; + 否则返回 None(空闲,或锁已过期可接管)。供 TUI 判断"原地 / 弹窗拷贝"。""" + lf = _lock_path(log_path) + meta = _read_lock(lf) + if meta is not None and _lock_fresh(lf): + return meta + return None + + +def acquire_lock(log_path, agent_id=None): + """尝试独占 `log_path`。成功(或锁设施故障降级)返回 True; + 仅当被另一活进程(心跳新鲜)持有时返回 False。""" + try: + os.makedirs(_LOCK_DIR, exist_ok=True) + lf = _lock_path(log_path) + meta = {'pid': os.getpid(), 'agent_id': agent_id, + 'log': os.path.basename(log_path), + 'started': time.time()} + blob = json.dumps(meta, ensure_ascii=False) + try: + fd = os.open(lf, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) + with os.fdopen(fd, 'w', encoding='utf-8') as fh: + fh.write(blob) + except FileExistsError: + cur = _read_lock(lf) + if cur and cur.get('pid') != os.getpid() and _lock_fresh(lf): + return False # 被另一活进程持有 + # 过期锁 / 本进程自己的 → 接管(覆盖)。小竞态窗口可接受。 + try: os.remove(lf) + except OSError: pass + try: + fd = os.open(lf, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) + with os.fdopen(fd, 'w', encoding='utf-8') as fh: + fh.write(blob) + except FileExistsError: + cur2 = _read_lock(lf) + if cur2 and cur2.get('pid') != os.getpid() and _lock_fresh(lf): + return False # 抢锁竞态输了 + with open(lf, 'w', encoding='utf-8') as fh: + fh.write(blob) + with _hb_lock: + _held_locks.add(log_path) + _ensure_hb_thread() + return True + except Exception: + # 锁设施故障绝不阻断续接 —— 降级为"假定空闲、放行"。 + return True + + +def release_lock(log_path): + """释放本进程对 `log_path` 的锁(只删自己持有/无主的锁文件)。""" + with _hb_lock: + _held_locks.discard(log_path) + try: + lf = _lock_path(log_path) + cur = _read_lock(lf) + if cur is None or cur.get('pid') == os.getpid(): + os.remove(lf) + except Exception: + pass + + +def _hb_tick(): + now = time.time() + with _hb_lock: + items = list(_held_locks) + for lp in items: + try: + os.utime(_lock_path(lp), (now, now)) # 仅更新 mtime,无 fsync + except OSError: + pass + + +def _hb_loop(): + while True: + time.sleep(_HB_INTERVAL) + try: + _hb_tick() + except Exception: + pass + + +def _ensure_hb_thread(): + global _hb_thread + with _hb_lock: + if _hb_thread is None: + _hb_thread = threading.Thread(target=_hb_loop, + name='ga-session-heartbeat', daemon=True) + _hb_thread.start() + + +@atexit.register +def _release_all_locks(): + for lp in list(_held_locks): + release_lock(lp) + + +def _new_log_path(): + """铸一个新的 6 位 logid 日志路径(与 agentmain 同公式)。""" + logid = f'{(time.time_ns() + random.randrange(1_000_000)) % 1_000_000:06d}' + return os.path.join(_LOG_DIR, f'model_responses_{logid}.txt') + + +def _retarget_log(agent, new_path): + """把 agent(及其所有 llmclient)的日志写入点切到 new_path —— 之后的轮次写这里。""" + try: + agent.log_path = new_path + except Exception: + pass + for client in _agent_clients(agent): + try: client.log_path = new_path + except Exception: pass + + +def is_snapshot(path): + """遗留快照存档(model_responses_snapshot_*.txt)。这类只能拷贝续,不参与原地 + (provisional,待 worktree 复审)。""" + return os.path.basename(path).startswith('model_responses_snapshot_') + + +def _clear_conversation_state(agent): + """清空对话状态(对齐 reset_conversation,但不碰日志文件)。""" + if hasattr(agent, 'history'): + agent.history = [] + for client in _agent_clients(agent): + backend = getattr(client, 'backend', None) + if backend is not None and hasattr(backend, 'history'): + backend.history = [] + if hasattr(client, 'last_tools'): + client.last_tools = '' + if hasattr(agent, 'handler'): + agent.handler = None + + +def acquire_birth_lock(agent, agent_id=None): + """会话出生时持有自己当前日志的锁(新 logid 必然抢到)。TUI 在建会话时调用, + 使本会话对"占用检测"可见 —— 别的会话才能据此判定它是否还活着。""" + lp = getattr(agent, 'log_path', '') or '' + if lp: + acquire_lock(lp, agent_id) + + +def release_current(agent): + """切走:释放 agent 当前日志的锁,旧日志原样留作"空闲会话"(不存快照、不清空)。""" + lp = getattr(agent, 'log_path', '') or '' + if lp: + release_lock(lp) + + +def begin_fresh_session(agent, agent_id=None): + """新对话 / clear:释放当前锁(旧日志留作空闲会话)→ 铸新 logid 重指 → 持新锁 → + 清空对话状态。**替代 TUI 里的 reset_conversation**(不再存快照/清空旧日志)。""" + try: agent.abort() + except Exception: pass + release_current(agent) + newp = _new_log_path() + _retarget_log(agent, newp) + acquire_lock(newp, agent_id) + _clear_conversation_state(agent) + + +def _load_history_into(agent, path, restore_wm=False): + """把 `path` 解析进 backend.history(native;否则降级摘要)。镜像 restore() 的解析, + 但不 abort/不快照(日志重指由调用方先做好)。返回 (msg, is_full)。 + + `restore_wm`(默认 False,仅显式传入的调用方启用,如 worldline TUI):从同一份日志 + 派生 history_info 写回 `agent.history`,使续接后工作记忆不丢。默认关闭 → 其它前端 + 行为完全不变。""" + try: + with open(path, encoding='utf-8', errors='replace') as fh: + content = fh.read() + except Exception as e: + return f'❌ 读取失败: {e}', False + pairs = _pairs(content) + if not pairs: + return f'❌ {os.path.basename(path)} 为空或格式不符', False + history = _parse_native_history(pairs) + name = os.path.basename(path) + if history is not None: + _replace_backend_history(agent, history) + if restore_wm and hasattr(agent, 'history'): + agent.history = _derive_hist_info(history) # 续接恢复工作记忆(opt-in) + return f'✅ 已恢复 {len(pairs)} 轮完整对话({name})', True + from chatapp_common import _restore_native_history, _restore_text_pairs + summary = _restore_text_pairs(content) or _restore_native_history(content) + if not summary: + return f'❌ {name} 无法解析(非 native 且无摘要可提取)', False + if hasattr(agent, 'history'): + agent.history.extend(summary) + n = sum(1 for l in summary if l.startswith('[USER]: ')) + return f'⚠️ 非 native 格式,降级恢复 {n} 轮摘要({name})', False + + +def _is_empty_log(path): + """日志空(<32 字节)或缺失。用于 allow_empty:回退到会话起点后日志被清空的会话。""" + try: + return os.path.getsize(path) < 32 + except OSError: + return True + + +def continue_inplace(agent, path, agent_id=None, allow_empty=False, restore_wm=False): + """原地续:把 agent 的日志指回 `path` 本身,之后轮次追加到 X,延续同一会话。 + 调用方应已确认空闲(session_occupant 为 None);抢锁失败(被占)返回错误。 + `allow_empty`(仅 worldline UI 传):日志为空时不报错,按【空会话】恢复(清空对话, + 由调用方按 `.ga_rewind` 树重连),用于"回退至会话起点"的会话。 + `restore_wm`(opt-in):续接后从日志派生 history_info 恢复工作记忆。返回 (msg, ok)。""" + try: agent.abort() + except Exception: pass + if not acquire_lock(path, agent_id): # 先抢到目标锁;失败则保持现状,不丢自己的锁 + return '❌ 会话已被占用,无法原地接管', False + cur = getattr(agent, 'log_path', '') or '' + if cur and os.path.basename(cur) != os.path.basename(path): + release_lock(cur) # 目标到手,旧会话释放为空闲(同一文件则不放) + _retarget_log(agent, path) + msg, ok = _load_history_into(agent, path, restore_wm=restore_wm) + if not ok and allow_empty and _is_empty_log(path): + _replace_backend_history(agent, []) # 空会话:清空对话(载入失败时它没被清) + return '✅ 已恢复空会话(回退至会话起点;世界线树已重连)', True + return msg, ok + + +def continue_copy(agent, path, agent_id=None, allow_empty=False, restore_wm=False): + """拷贝续:铸新 logid、把 `path` 内容拷进去,在副本上续;`path` 原件不动。 + 用于"被占用→用户选拷贝"以及快照源。`restore_wm`(opt-in)同 continue_inplace。 + 返回 (msg, ok)。""" + try: agent.abort() + except Exception: pass + release_current(agent) + newp = _new_log_path() + try: + shutil.copyfile(path, newp) + except Exception: + pass + acquire_lock(newp, agent_id) + _retarget_log(agent, newp) + msg, ok = _load_history_into(agent, newp, restore_wm=restore_wm) + if not ok and allow_empty and _is_empty_log(newp): + _replace_backend_history(agent, []) + return '✅ 已恢复空会话(回退至会话起点;世界线树已重连)', True + return msg, ok + + +def install(cls): + """Wrap cls._handle_slash_cmd so /continue is handled before original dispatch.""" + orig = cls._handle_slash_cmd + if getattr(orig, '_continue_patched', False): return + def patched(self, raw_query, display_queue): + if (raw_query or '').startswith('/continue'): + r = handle(self, raw_query, display_queue) + if r is None: return None + return orig(self, raw_query, display_queue) + patched._continue_patched = True + cls._handle_slash_cmd = patched diff --git a/frontends/cost_tracker.py b/frontends/cost_tracker.py new file mode 100644 index 000000000..0471129fb --- /dev/null +++ b/frontends/cost_tracker.py @@ -0,0 +1,420 @@ +"""Per-thread LLM token usage via llmcore monkey-patches. + +`install()` wraps `llmcore._record_usage` + `llmcore.print` (the SSE +`messages` path only emits final `output_tokens` through `[Output] tokens=N`). +Trackers are keyed by `threading.current_thread().name`; each TUI session +runs its agent on `ga-tui-agent-`, so `/cost` is a thread lookup. + +Subagent processes are out-of-process, so `scan_subagent_logs` parses the +same `[Cache]` / `[Output]` print lines from `temp/*/stdout.log`. +""" +from __future__ import annotations +import glob, json, os, re, threading, time +from dataclasses import dataclass, field +from pathlib import Path + + +@dataclass +class TokenStats: + requests: int = 0 + input: int = 0 + output: int = 0 + cache_create: int = 0 + cache_read: int = 0 + # Latest single-LLM-call sizes — drive the spinner's `↑ N · ↓ M`. + last_input: int = 0 + last_output: int = 0 + started_at: float = field(default_factory=time.time) + + def total_input_side(self) -> int: + return self.input + self.cache_create + self.cache_read + + def total_tokens(self) -> int: + return self.input + self.output + self.cache_create + self.cache_read + + def cache_hit_rate(self) -> float: + side = self.total_input_side() + return (self.cache_read / side * 100.0) if side else 0.0 + + def elapsed_seconds(self) -> float: + return max(0.0, time.time() - self.started_at) + + +# GA's real context budget lives on `BaseSession.context_win` (chars). The +# trim trigger is `context_win * 3` (see llmcore.trim_messages_history), so +# `/cost` compares actual-history chars against that cap for consistent units. +def context_window_chars(backend) -> int: + """`context_win * 3` — the char cap before `trim_messages_history` kicks + in. Reads dynamically so a `mykey.py` override propagates. Returns 0 on + bad/missing backend so the caller can hide the row.""" + try: + return int(getattr(backend, 'context_win', 0)) * 3 + except (TypeError, ValueError): + return 0 + + +def current_input_chars(backend) -> int: + """Char-size of the message history (same unit as `trim_messages_history`).""" + try: + import json as _json + history = getattr(backend, 'history', None) or [] + return sum(len(_json.dumps(m, ensure_ascii=False)) for m in history) + except Exception: + return 0 + + +_trackers: dict[str, TokenStats] = {} +_lock = threading.Lock() +_OUT_RE = re.compile(r'\[Output\]\s+tokens=(\d+)') +_CACHE_RE_NEW = re.compile(r'\[Cache\]\s+input=(\d+)\s+creation=(\d+)\s+read=(\d+)') +_CACHE_RE_OLD = re.compile(r'\[Cache\]\s+input=(\d+)\s+cached=(\d+)') +_INSTALLED = False +_SUBAGENT_GLOB = os.path.join("temp", "*", "stdout.log") + +# ── Per-call ledger ────────────────────────────────────────────────────────── + +_ledger_path: Path | None = None +_ledger_fd = None +_ledger_lock = threading.RLock() +_ledger_uncompacted_bytes = 0 +_LEDGER_FILENAME = "token_ledger.jsonl" +_LEGACY_HISTORY_FILENAME = "desktop_token_history.json" +_COMPACT_THRESHOLD = 10 * 1024 * 1024 # 10MB + + +def _migrate_legacy_history_unlocked() -> None: + """Seed an empty ledger from the previous aggregate JSON format once.""" + global _ledger_uncompacted_bytes + if _ledger_path is None or _ledger_fd is None: + return + legacy_path = _ledger_path.with_name(_LEGACY_HISTORY_FILENAME) + try: + if _ledger_path.stat().st_size or not legacy_path.is_file(): + return + doc = json.loads(legacy_path.read_text(encoding="utf-8")) + if not isinstance(doc, dict) or not isinstance(doc.get("snap"), dict): + return + metadata: dict[str, dict] = {} + for entry in doc.get("history", []): + if not isinstance(entry, dict): + continue + sid = entry.get("sessionId") or entry.get("id") + if not isinstance(sid, str) or not sid: + continue + key = sid if sid.startswith("GA-") else f"GA-{sid}" + try: + ts = float(entry.get("ts", 0) or 0) + except (TypeError, ValueError): + ts = 0 + if key not in metadata or ts >= metadata[key]["ts"]: + metadata[key] = { + "ts": ts, + "model": entry.get("model") if isinstance(entry.get("model"), str) else "", + "title": entry.get("title") if isinstance(entry.get("title"), str) else "", + } + fallback_ts = legacy_path.stat().st_mtime + for key, totals in doc["snap"].items(): + if not isinstance(key, str) or not key or not isinstance(totals, dict): + continue + try: + values = { + "i": int(totals.get("input", 0) or 0), + "o": int(totals.get("output", 0) or 0), + "cc": int(totals.get("cacheCreate", totals.get("cacheWrite", 0)) or 0), + "cr": int(totals.get("cacheRead", 0) or 0), + } + except (TypeError, ValueError): + continue + meta = metadata.get(key, {}) + line = json.dumps( + {"t": meta.get("ts") or fallback_ts, "k": key, **values, + "m": meta.get("model", ""), "n": meta.get("title", ""), "_migrated": True}, + separators=(",", ":"), + ) + "\n" + _ledger_fd.write(line) + _ledger_uncompacted_bytes += len(line.encode("utf-8")) + _ledger_fd.flush() + except Exception: + return + + +def init_ledger(root: str) -> None: + """Call once at bridge startup to set the ledger file path.""" + global _ledger_path, _ledger_fd, _ledger_uncompacted_bytes + with _ledger_lock: + if _ledger_fd is not None: + try: + _ledger_fd.close() + except Exception: + pass + _ledger_path = Path(root) / "temp" / _LEDGER_FILENAME + _ledger_path.parent.mkdir(parents=True, exist_ok=True) + _ledger_fd = open(_ledger_path, "a", encoding="utf-8") + try: + _ledger_uncompacted_bytes = _ledger_path.stat().st_size + if _ledger_uncompacted_bytes == 0: + _migrate_legacy_history_unlocked() + if _ledger_uncompacted_bytes >= _COMPACT_THRESHOLD: + _compact_ledger() + except OSError: + _ledger_uncompacted_bytes = 0 + + +def _append_ledger(thread_key: str, inp: int, out: int, cc: int, cr: int) -> None: + global _ledger_uncompacted_bytes + line = json.dumps( + {"t": time.time(), "k": thread_key, "i": inp, "o": out, "cc": cc, "cr": cr}, + separators=(",", ":"), + ) + "\n" + with _ledger_lock: + if _ledger_fd is None: + return + try: + _ledger_fd.write(line) + _ledger_fd.flush() + _ledger_uncompacted_bytes += len(line.encode("utf-8")) + if _ledger_uncompacted_bytes >= _COMPACT_THRESHOLD: + _compact_ledger() + except Exception: + pass + + +def _iter_ledger_unlocked(): + if _ledger_path is None or not _ledger_path.is_file(): + return + try: + with open(_ledger_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + if isinstance(entry, dict): + yield entry + except (json.JSONDecodeError, ValueError): + continue + except OSError: + return + + +def read_ledger() -> list[dict]: + """Read all valid lines from the ledger. Skips corrupted lines.""" + with _ledger_lock: + return list(_iter_ledger_unlocked()) + + +def _aggregate_sessions(entries) -> dict[str, dict]: + sessions: dict[str, dict] = {} + for e in entries: + k = e.get("k", "") + if not isinstance(k, str) or not k: + continue + try: + ts = float(e.get("t", 0) or 0) + inp = int(e.get("i", 0) or 0) + out = int(e.get("o", 0) or 0) + cc = int(e.get("cc", 0) or 0) + cr = int(e.get("cr", 0) or 0) + except (TypeError, ValueError): + continue + if k not in sessions: + sessions[k] = {"input": 0, "output": 0, "cacheCreate": 0, "cacheRead": 0, + "model": "", "title": "", "first_ts": ts, "last_ts": ts} + s = sessions[k] + s["input"] += inp + s["output"] += out + s["cacheCreate"] += cc + s["cacheRead"] += cr + s["first_ts"] = min(s["first_ts"], ts) + s["last_ts"] = max(s["last_ts"], ts) + if isinstance(e.get("m"), str) and e["m"]: + s["model"] = e["m"] + if isinstance(e.get("n"), str) and e["n"]: + s["title"] = e["n"] + return sessions + + +def _format_aggregate(sessions: dict[str, dict]) -> dict: + history = [] + snap = {} + for k, s in sessions.items(): + sid = k.removeprefix("GA-") + history.append({ + "sessionId": sid, + "title": s["title"] or sid, + "input": s["input"], + "output": s["output"], + "cacheCreate": s["cacheCreate"], + "cacheRead": s["cacheRead"], + "model": s["model"], + "ts": s["last_ts"], + }) + snap[k] = { + "input": s["input"], + "output": s["output"], + "cacheCreate": s["cacheCreate"], + "cacheRead": s["cacheRead"], + } + return {"history": history, "snap": snap} + + +def aggregate_ledger() -> dict: + """Aggregate ledger into {history: [...], snap: {...}} for /token-history.""" + with _ledger_lock: + return _format_aggregate(_aggregate_sessions(_iter_ledger_unlocked())) + + +def _compact_ledger() -> None: + """Compact ledger by aggregating into per-session totals and rewriting.""" + if _ledger_path is None: + return + global _ledger_fd, _ledger_uncompacted_bytes + with _ledger_lock: + temp_path = _ledger_path.with_suffix(".jsonl.tmp") + try: + if _ledger_fd is not None: + _ledger_fd.flush() + sessions = _aggregate_sessions(_iter_ledger_unlocked()) + with open(temp_path, "w", encoding="utf-8") as f: + for k, s in sessions.items(): + line = json.dumps( + {"t": s["last_ts"], "k": k, "i": s["input"], "o": s["output"], + "cc": s["cacheCreate"], "cr": s["cacheRead"], "m": s["model"], + "n": s["title"], + "_compacted": True}, + separators=(",", ":"), + ) + f.write(line + "\n") + f.flush() + os.fsync(f.fileno()) + if _ledger_fd is not None: + _ledger_fd.close() + _ledger_fd = None + os.replace(temp_path, _ledger_path) + _ledger_fd = open(_ledger_path, "a", encoding="utf-8") + _ledger_uncompacted_bytes = 0 + except Exception: + try: + temp_path.unlink(missing_ok=True) + except Exception: + pass + if _ledger_fd is None: + try: + _ledger_fd = open(_ledger_path, "a", encoding="utf-8") + except Exception: + pass + + +# ── Core API ───────────────────────────────────────────────────────────────── + +def scan_subagent_logs(since: float = 0.0, root: str | None = None) -> TokenStats: + """Aggregate subagent tokens from `temp//stdout.log` files; pass + `since=tui_start_time` to scope to this run. Best-effort: bad logs skipped.""" + out = TokenStats() + if since > 0: out.started_at = since + pattern = os.path.join(root, _SUBAGENT_GLOB) if root else _SUBAGENT_GLOB + for p in glob.glob(pattern): + try: + if since and os.path.getmtime(p) < since: continue + with open(p, encoding="utf-8", errors="ignore") as f: + for line in f: + if line.startswith("[Output]"): + m = _OUT_RE.match(line) + if m: + out.output += int(m.group(1)); out.requests += 1 + elif line.startswith("[Cache]"): + # messages → `input=N creation=C read=R` (input excl. cache); + # chat_completions / responses → `input=N cached=R` (input incl. cached). + m = _CACHE_RE_NEW.match(line) + if m: + i, c, r = int(m.group(1)), int(m.group(2)), int(m.group(3)) + out.input += i + out.cache_create += c; out.cache_read += r + continue + m = _CACHE_RE_OLD.match(line) + if m: + i, r = int(m.group(1)), int(m.group(2)) + out.input += max(0, i - r); out.cache_read += r + except OSError: + continue + return out + + +def get(thread_name: str) -> TokenStats: + with _lock: + if thread_name not in _trackers: + _trackers[thread_name] = TokenStats() + return _trackers[thread_name] + + +def reset(thread_name: str) -> None: + with _lock: + _trackers.pop(thread_name, None) + + +def all_trackers() -> dict[str, TokenStats]: + with _lock: + return dict(_trackers) + + +def install() -> None: + """Idempotently wrap llmcore._record_usage and llmcore.print.""" + global _INSTALLED + if _INSTALLED: return + import llmcore + orig_record, orig_print = llmcore._record_usage, print + + def record_patched(usage, api_mode): + # Handles INPUT / CACHE only; OUTPUT comes via `[Output]` print_patched + # below (the SSE path emits it that way; double-counting was the prior bug). + try: + if usage: + t = get(threading.current_thread().name) + t.requests += 1 + inp = cc = cr = 0 + if api_mode == 'messages': + inp = int(usage.get('input_tokens', 0) or 0) + cc = int(usage.get('cache_creation_input_tokens', 0) or 0) + cr = int(usage.get('cache_read_input_tokens', 0) or 0) + t.input += inp; t.cache_create += cc; t.cache_read += cr + # Non-stream `messages` skips the [Output] print, so count + # output_tokens here; SSE message_start carries a 1-token + # placeholder to skip. + out = int(usage.get('output_tokens', 0) or 0) + if out > 1: + t.output += out; t.last_output = out + _append_ledger(threading.current_thread().name, inp, out, cc, cr) + else: + _append_ledger(threading.current_thread().name, inp, 0, cc, cr) + t.last_input = inp + cc + cr + elif api_mode == 'chat_completions': + cached = int((usage.get('prompt_tokens_details') or {}).get('cached_tokens', 0) or 0) + inp = int(usage.get('prompt_tokens', 0) or 0) - cached + t.input += inp; t.cache_read += cached + t.last_input = inp + cached + _append_ledger(threading.current_thread().name, inp, 0, 0, cached) + elif api_mode == 'responses': + cached = int((usage.get('input_tokens_details') or {}).get('cached_tokens', 0) or 0) + inp = int(usage.get('input_tokens', 0) or 0) - cached + t.input += inp; t.cache_read += cached + t.last_input = inp + cached + _append_ledger(threading.current_thread().name, inp, 0, 0, cached) + except Exception: pass + return orig_record(usage, api_mode) + llmcore._record_usage = record_patched + + def print_patched(*args, **kwargs): + try: + if args and isinstance(args[0], str): + m = _OUT_RE.match(args[0]) + if m: + t = get(threading.current_thread().name) + n = int(m.group(1)) + t.output += n; t.last_output = n + _append_ledger(threading.current_thread().name, 0, n, 0, 0) + except Exception: pass + return orig_print(*args, **kwargs) + llmcore.print = print_patched + + _INSTALLED = True diff --git a/frontends/dcapp.py b/frontends/dcapp.py new file mode 100644 index 000000000..481196d8c --- /dev/null +++ b/frontends/dcapp.py @@ -0,0 +1,407 @@ +# Discord Bot Frontend for GenericAgent +# ⚠️ 需要在 Discord Developer Portal 开启 "Message Content Intent" +# Bot → Privileged Gateway Intents → MESSAGE CONTENT INTENT → 打开 +# pip install discord.py + +import asyncio, json, os, queue as Q, re, sys, threading, time +from collections import OrderedDict + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from agentmain import GeneraticAgent +from chatapp_common import ( + AgentChatMixin, build_done_text, ensure_single_instance, extract_files, + public_access, redirect_log, require_runtime, split_text, strip_files, clean_reply, + HELP_TEXT, FILE_HINT, format_restore, + _handle_continue_frontend, _reset_conversation, +) +from llmcore import mykeys + +try: + import discord +except Exception: + print("Please install discord.py to use Discord: pip install discord.py") + sys.exit(1) + +agent = GeneraticAgent(); agent.verbose = False +BOT_TOKEN = str(mykeys.get("discord_bot_token", "") or "").strip() +ALLOWED = {str(x).strip() for x in mykeys.get("discord_allowed_users", []) if str(x).strip()} +USER_TASKS = {} +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +TEMP_DIR = os.path.join(PROJECT_ROOT, "temp") +MEDIA_DIR = os.path.join(TEMP_DIR, "discord_media") +ACTIVE_FILE = os.path.join(TEMP_DIR, "discord_active_channels.json") +ACTIVE_TTL_SECONDS = 30 * 24 * 3600 +EXIT_CHANNEL_TEXTS = {"退出该频道", "退出此频道", "退出频道"} +EXIT_THREAD_TEXTS = {"退出该子区", "退出此子区", "退出子区"} +os.makedirs(MEDIA_DIR, exist_ok=True) + + +def _extract_discord_progress(text): + """Return the newest concise from a streaming transcript.""" + matches = re.findall(r"\s*(.*?)\s*", text or "", flags=re.DOTALL) + if not matches: + return "" + summary = re.sub(r"\s+", " ", matches[-1]).strip() + return summary[:120] + + +def _strip_discord_transcript(text): + """Hide LLM/tool transcript noise while preserving the final natural reply.""" + text = text or "" + text = re.sub(r"^\s*\*?\*?LLM Running \(Turn \d+\) \.\.\.\*?\*?\s*$", "", text, flags=re.M) + text = re.sub(r"^\s*🛠️\s+.*?(?=^\s*(?:\*?\*?LLM Running||$))", "", text, flags=re.M | re.DOTALL) + text = re.sub(r"^\s*(?:✅|❌|ERR|STDOUT|PAT\b|RC\b).*?$", "", text, flags=re.M) + text = re.sub(r".*?", "", text, flags=re.DOTALL) + text = clean_reply(text) + return strip_files(text).strip() + + +def _display_done_text(text): + body = _strip_discord_transcript(text) + if body and body != "...": + return body + summaries = re.findall(r"\s*(.*?)\s*", text or "", flags=re.DOTALL) + if summaries: + return re.sub(r"\s+", " ", summaries[-1]).strip() or "..." + return "..." + + +class DiscordApp(AgentChatMixin): + label, source, split_limit = "Discord", "discord", 1900 + + def __init__(self): + super().__init__(agent, USER_TASKS) + intents = discord.Intents.default() + intents.message_content = True + intents.guilds = True + intents.dm_messages = True + proxy = str(mykeys.get("proxy", "") or "").strip() or None + self.client = discord.Client(intents=intents, proxy=proxy) + self.background_tasks = set() + self._channel_cache = OrderedDict() # chat_id -> channel/user object (LRU, max 500) + self._active_channels = self._load_active_channels() # guild chat_id -> {last_seen: float} + self._active_lock = threading.Lock() + self._agents = OrderedDict() # chat_id -> GeneraticAgent, each chat has isolated history + self._agent_lock = threading.Lock() + + @self.client.event + async def on_ready(): + print(f"[Discord] bot ready: {self.client.user} ({self.client.user.id})") + + @self.client.event + async def on_message(message): + await self._handle_message(message) + + def _chat_id(self, message): + """Return a string chat_id: 'dm:' or 'ch:'.""" + if isinstance(message.channel, discord.DMChannel): + return f"dm:{message.author.id}" + return f"ch:{message.channel.id}" + + def _load_active_channels(self): + try: + with open(ACTIVE_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + if not isinstance(data, dict): + return {} + now = time.time() + active = {} + for chat_id, item in data.items(): + if not str(chat_id).startswith("ch:") or not isinstance(item, dict): + continue + last_seen = float(item.get("last_seen") or 0) + if now - last_seen <= ACTIVE_TTL_SECONDS: + active[str(chat_id)] = {"last_seen": last_seen} + return active + except FileNotFoundError: + return {} + except Exception as e: + print(f"[Discord] failed to load active channels: {e}") + return {} + + def _save_active_channels(self): + try: + os.makedirs(os.path.dirname(ACTIVE_FILE), exist_ok=True) + tmp = ACTIVE_FILE + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(self._active_channels, f, ensure_ascii=False, indent=2, sort_keys=True) + os.replace(tmp, ACTIVE_FILE) + except Exception as e: + print(f"[Discord] failed to save active channels: {e}") + + def _is_active_channel(self, chat_id, now=None): + now = now or time.time() + with self._active_lock: + item = self._active_channels.get(chat_id) + if not item: + return False + if now - float(item.get("last_seen") or 0) > ACTIVE_TTL_SECONDS: + self._active_channels.pop(chat_id, None) + self._save_active_channels() + print(f"[Discord] channel expired: {chat_id}") + return False + return True + + def _touch_active_channel(self, chat_id, now=None): + if not chat_id.startswith("ch:"): + return + with self._active_lock: + self._active_channels[chat_id] = {"last_seen": float(now or time.time())} + self._save_active_channels() + + def _deactivate_channel(self, chat_id): + with self._active_lock: + changed = self._active_channels.pop(chat_id, None) is not None + self._save_active_channels() + state = self.user_tasks.get(chat_id) + if state: + state["running"] = False + try: + self._get_agent(chat_id).abort() + except Exception as e: + print(f"[Discord] deactivate abort failed for {chat_id}: {e}") + return changed + + def _get_agent(self, chat_id): + with self._agent_lock: + ga = self._agents.get(chat_id) + if ga is None: + ga = GeneraticAgent() + ga.verbose = False + self._agents[chat_id] = ga + threading.Thread(target=ga.run, daemon=True, name=f"discord-agent-{chat_id}").start() + if len(self._agents) > 200: + old_chat_id, _old_agent = self._agents.popitem(last=False) + print(f"[Discord] dropped agent cache entry: {old_chat_id}") + else: + self._agents.move_to_end(chat_id) + return ga + + async def _download_attachments(self, message): + """Download attachments/images to MEDIA_DIR, return list of local paths.""" + paths = [] + for att in message.attachments: + safe_name = re.sub(r'[<>:"/\\|?*]', '_', att.filename or f"file_{att.id}") + local_path = os.path.join(MEDIA_DIR, f"{att.id}_{safe_name}") + try: + await att.save(local_path) + paths.append(local_path) + print(f"[Discord] saved attachment: {local_path}") + except Exception as e: + print(f"[Discord] failed to save attachment {att.filename}: {e}") + return paths + + async def send_text(self, chat_id, content, **ctx): + """Send text (and optionally files) to a chat_id.""" + channel = self._channel_cache.get(chat_id) + if channel is None: + try: + if chat_id.startswith("dm:"): + user = await self.client.fetch_user(int(chat_id[3:])) + channel = await user.create_dm() + else: + channel = await self.client.fetch_channel(int(chat_id[3:])) + self._channel_cache[chat_id] = channel + if len(self._channel_cache) > 500: + self._channel_cache.popitem(last=False) + except Exception as e: + print(f"[Discord] cannot resolve channel for {chat_id}: {e}") + return + for part in split_text(content, self.split_limit): + try: + await channel.send(part) + except Exception as e: + print(f"[Discord] send error: {e}") + + async def send_done(self, chat_id, raw_text, **ctx): + """Send final reply: text parts + file attachments.""" + files = [p for p in extract_files(raw_text) if os.path.exists(p)] + body = _display_done_text(raw_text) + + # Send text (send_text handles splitting internally) + if body and body != "...": + await self.send_text(chat_id, body, **ctx) + + # Send files as Discord attachments + if files: + channel = self._channel_cache.get(chat_id) + if channel: + for fpath in files: + try: + await channel.send(file=discord.File(fpath)) + except Exception as e: + print(f"[Discord] failed to send file {fpath}: {e}") + await self.send_text(chat_id, f"⚠️ 文件发送失败: {os.path.basename(fpath)}", **ctx) + + if not body and not files: + await self.send_text(chat_id, "...", **ctx) + + async def handle_command(self, chat_id, cmd, **ctx): + """Handle slash commands against the per-chat agent, keeping Discord chats isolated.""" + ga = self._get_agent(chat_id) + parts = (cmd or "").split() + op = (parts[0] if parts else "").lower() + if op == "/help": + return await self.send_text(chat_id, HELP_TEXT, **ctx) + if op == "/stop": + state = self.user_tasks.get(chat_id) + if state: + state["running"] = False + ga.abort() + return await self.send_text(chat_id, "⏹️ 正在停止...", **ctx) + if op == "/status": + llm = ga.get_llm_name() if ga.llmclient else "未配置" + return await self.send_text(chat_id, f"状态: {'🔴 运行中' if ga.is_running else '🟢 空闲'}\nLLM: [{ga.llm_no}] {llm}", **ctx) + if op == "/llm": + if not ga.llmclient: + return await self.send_text(chat_id, "❌ 当前没有可用的 LLM 配置", **ctx) + if len(parts) > 1: + try: + ga.next_llm(int(parts[1])) + return await self.send_text(chat_id, f"✅ 已切换到 [{ga.llm_no}] {ga.get_llm_name()}", **ctx) + except Exception: + return await self.send_text(chat_id, f"用法: /llm <0-{len(ga.list_llms()) - 1}>", **ctx) + lines = [f"{'→' if cur else ' '} [{i}] {name}" for i, name, cur in ga.list_llms()] + return await self.send_text(chat_id, "LLMs:\n" + "\n".join(lines), **ctx) + if op == "/restore": + try: + restored_info, err = format_restore() + if err: + return await self.send_text(chat_id, err, **ctx) + restored, fname, count = restored_info + ga.abort() + ga.history.extend(restored) + return await self.send_text(chat_id, f"✅ 已恢复 {count} 轮对话\n来源: {fname}\n(仅恢复上下文,请输入新问题继续)", **ctx) + except Exception as e: + return await self.send_text(chat_id, f"❌ 恢复失败: {e}", **ctx) + if op == "/continue": + return await self.send_text(chat_id, _handle_continue_frontend(ga, cmd), **ctx) + if op == "/new": + return await self.send_text(chat_id, _reset_conversation(ga), **ctx) + return await self.send_text(chat_id, HELP_TEXT, **ctx) + + async def run_agent(self, chat_id, text, **ctx): + """Run the isolated per-chat Discord agent.""" + ga = self._get_agent(chat_id) + state = {"running": True} + self.user_tasks[chat_id] = state + try: + await self.send_text(chat_id, "思考中...", **ctx) + dq = ga.put_task(f"{FILE_HINT}\n\n{text}", source=self.source) + last_ping = time.time() + last_step = "" + step_no = 0 + while state["running"]: + try: + item = await asyncio.to_thread(dq.get, True, 3) + except Q.Empty: + if ga.is_running and time.time() - last_ping > self.ping_interval: + await self.send_text(chat_id, "⏳ 还在处理中,请稍等...", **ctx) + last_ping = time.time() + continue + if "next" in item: + step = _extract_discord_progress(item.get("next", "")) + if step and step != last_step: + step_no += 1 + await self.send_text(chat_id, f"步骤{step_no}:{step}", **ctx) + last_step = step + last_ping = time.time() + continue + if "done" in item: + await self.send_done(chat_id, item.get("done", ""), **ctx) + break + if not state["running"]: + await self.send_text(chat_id, "⏹️ 已停止", **ctx) + except Exception as e: + import traceback + print(f"[{self.label}] run_agent error: {e}") + traceback.print_exc() + await self.send_text(chat_id, f"❌ 错误: {e}", **ctx) + finally: + self.user_tasks.pop(chat_id, None) + + async def _handle_message(self, message): + # Ignore self + if message.author == self.client.user or message.author.bot: + return + + is_dm = isinstance(message.channel, discord.DMChannel) + is_guild = message.guild is not None + chat_id = self._chat_id(message) + now = time.time() + mentioned = bool(is_guild and self.client.user and self.client.user.mentioned_in(message)) + + self._channel_cache[chat_id] = message.channel + if len(self._channel_cache) > 500: + self._channel_cache.popitem(last=False) + + user_id = str(message.author.id) + user_name = str(message.author) + + if not public_access(ALLOWED) and user_id not in ALLOWED: + print(f"[Discord] unauthorized user: {user_name} ({user_id})") + return + + if is_guild: + active = self._is_active_channel(chat_id, now) + if not mentioned and not active: + return + if mentioned or active: + self._touch_active_channel(chat_id, now) + + # Strip bot mention from content + content = message.content or "" + if is_guild and self.client.user: + content = re.sub(rf"<@!?{self.client.user.id}>", "", content).strip() + else: + content = content.strip() + + normalized = re.sub(r"\s+", "", content) + if is_guild and normalized in EXIT_CHANNEL_TEXTS | EXIT_THREAD_TEXTS: + self._deactivate_channel(chat_id) + label = "子区" if normalized in EXIT_THREAD_TEXTS else "频道" + await self.send_text(chat_id, f"✅ 已退出该{label},之后除非重新 @ 我,否则不会主动响应。") + print(f"[Discord] manually deactivated {chat_id} by {user_name} ({user_id})") + return + + # Download attachments + attachment_paths = await self._download_attachments(message) + + # Build message text with attachment paths + if attachment_paths: + paths_text = "\n".join(f"[附件: {p}]" for p in attachment_paths) + content = f"{content}\n{paths_text}" if content else paths_text + + if not content: + return + + print(f"[Discord] message from {user_name} ({user_id}, {'dm' if is_dm else 'guild'}): {content[:200]}") + + if content.startswith("/"): + return await self.handle_command(chat_id, content) + + task = asyncio.create_task(self.run_agent(chat_id, content)) + self.background_tasks.add(task) + task.add_done_callback(self.background_tasks.discard) + + async def start(self): + print("[Discord] bot starting...") + delay, max_delay = 5, 300 + while True: + started_at = time.monotonic() + try: + await self.client.start(BOT_TOKEN) + except Exception as e: + print(f"[Discord] error: {e}") + if time.monotonic() - started_at >= 60: + delay = 5 + print(f"[Discord] reconnect in {delay}s...") + await asyncio.sleep(delay) + delay = min(delay * 2, max_delay) + + +if __name__ == "__main__": + _LOCK_SOCK = ensure_single_instance(19532, "Discord") + require_runtime(agent, "Discord", discord_bot_token=BOT_TOKEN) + redirect_log(__file__, "dcapp.log", "Discord", ALLOWED) + asyncio.run(DiscordApp().start()) diff --git a/frontends/desktop/.gitignore b/frontends/desktop/.gitignore new file mode 100644 index 000000000..748ad2fe8 --- /dev/null +++ b/frontends/desktop/.gitignore @@ -0,0 +1,8 @@ +# Rust build artifacts +src-tauri/target/ +src-tauri/gen/ + +# Node +node_modules/ +dist/ +e2e-results/ diff --git a/frontends/desktop/.npmrc b/frontends/desktop/.npmrc new file mode 100644 index 000000000..521a9f7c0 --- /dev/null +++ b/frontends/desktop/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/frontends/desktop/DESIGN.md b/frontends/desktop/DESIGN.md new file mode 100644 index 000000000..ebc28833a --- /dev/null +++ b/frontends/desktop/DESIGN.md @@ -0,0 +1,275 @@ +# GenericAgent Desktop — 产品设计规范 + +本文档记录 Desktop 客户端已确立的产品设计语言,供 PM、设计师、前端开发者和 AI agent 在实现新功能时参照。 + +--- + +## 1. 功能架构分区 + +### 1.1 四域模型 + +Desktop 围绕四个功能域组织,每个域拥有独立的信息密度和交互节奏: + +| 域 | Page ID | 核心职责 | 信息密度 | 交互节奏 | +|----|---------|---------|---------|---------| +| 对话 | `chat` | 与 agent 的主交互界面 | 高(流式文本 + 代码 + 工具调用) | 实时 | +| 服务 | `services` | 后台服务进程的生命周期管理 | 中(状态卡片 + 日志) | 观察为主、偶尔操作 | +| 协作 | `collab` | Conductor 多代理编排 | 高(平行 worker 状态 + 对话) | 实时 | +| 用量 | `token` | Token 消耗统计与分析 | 低(表格 + 图表) | 回顾 | + +**设计原则**:每个域有且只有一种主要交互模式。Chat 是"对话流",Services 是"仪表盘",Collab 是"分屏指挥",Token 是"报表查阅"。新功能应归属到已有域,而非创造新域。 + +### 1.2 Shell 结构 + +``` +┌─ Titlebar (macOS: 系统流量灯 + 控制按钮; Windows: 自绘标题栏) ──────────────┐ +│ ┌─ Sidebar (260px, 可折叠 Cmd+B) ─┐ ┌─ MainArea (flex:1) ──────────────┐ │ +│ │ Navigation Rail (垂直图标) │ │ │ │ +│ │ Search Input │ │ [ChatView / ServicesPage / │ │ +│ │ Session List (历史会话) │ │ CollabPage / TokenPage] │ │ +│ │ ─── Footer ─── │ │ │ │ +│ │ Settings Gear │ │ │ │ +│ └──────────────────────────────────┘ └───────────────────────────────────┘ │ +├─ Statusbar (20px, 服务状态 + bridge 信息) ───────────────────────────────────┤ +└───────────────────────────────────────────────────────────────────────────────┘ +``` + +**Navigation Rail**:垂直排列的 Codicon 图标按钮,不带文字标签。当前激活项通过 `--ui-icon-nav-active` 颜色和左侧 2px accent 条指示。点击切换 MainArea 内容。 + +**Sidebar 与 MainArea 的关系**:Sidebar 是全局导航 + 对话历史的常驻通道,内容不随 page 切换改变。MainArea 独占具体功能域。两者通过 ResizeGroup 允许用户拖拽宽度(200–340px)。 + +### 1.3 产品层级决策 + +添加新功能时的路由规则: + +1. **能归入现有域吗?** 如果新功能是对话的增强(附件、快捷指令),放在 Chat 域内。如果是进程管理类,放在 Services。 +2. **需要全屏空间吗?** 如果是纯信息查看(日志、统计),考虑作为已有域的子视图而非新 page。 +3. **只有确定需要并行的、不可叠加的独立交互模式**时,才增加新域。增加域 = 修改 `PageId` union + Nav Rail + 路由逻辑。 + +--- + +## 2. 渐进式披露 (Progressive Disclosure) + +### 2.1 三层信息架构 + +所有用户面对的文本遵循三层结构: + +| 层级 | 角色 | 可见条件 | 长度约束 | +|------|------|---------|---------| +| **L1 扫视** | 用户即刻可见,帮助快速定位 | 始终可见 | 2–4 字 / 1–3 词 | +| **L2 解释** | 回答"这是什么/为什么" | 悬浮或 focus | 4–8 字 / 一短句 | +| **L3 诊断** | 完整文档级信息 | 主动展开(drawer/modal/展开区) | 不限 | + +**规则**:默认只展示 L1。用户不需要理解系统模型即可操作。L2 在好奇或犹豫时出现。L3 只在用户明确要求深入时出现。 + +### 2.2 各域的披露策略 + +#### Chat Thread + +| 元素 | 默认状态 | 展开触发 | 展开形态 | +|------|---------|---------|---------| +| 用户消息 | 4 行 clamp + 渐变遮罩 | 点击展开按钮 | 最大 40dvh 内滚动 | +| Thinking | 67% 透明度 + 折叠 | hover → 100% 透明度;点击展开 | `
    ` 模式,最大 10rem | +| Tool Call | 67% 透明度 + 仅显示 header | hover → 100%;点击展开 body | 最大 7.5rem | +| Code Block | 完整显示,内部 scroll | — | — | +| 复制按钮 | 隐藏 (opacity: 0) | hover 消息气泡 | 渐入 | + +**透明度规则**:非核心信息(thinking、tool call)初始 0.67 透明度,表示"可选关注"。用户 hover 时升至 1.0。这不是装饰,而是视觉权重管理——让用户的注意力默认流向 assistant 的文本回复。 + +#### Settings + +- 顶层:堆叠 block(`.ga-set-block`),一眼可见所有配置类别 +- 次层:Advanced toggle(展开/收起高级字段) +- 子视图:从 `main` 切到 `addModel` 等子 form 时,用 back-link 导航而非嵌套 modal + +#### Services + +- 默认:卡片列表显示服务名 + 状态指示 +- hover:显露操作按钮(start/stop/logs) +- 展开:日志模态窗(全高 modal) + +#### Statusbar + +- 默认:20px 高度,显示关键指标的 inline 摘要 +- 展开:点击 statusbar item → 向上弹出 popover panel(`.ga-bridge-panel`) +- Panel 内含多 tab(日志尾 + 操作按钮) + +### 2.3 容器选择决策树 + +``` +需要传达的信息 → +├─ 1-2 词补充说明 → Tooltip (L2) +├─ 一段描述但不打断当前 flow → Inline expand (details/toggle) +├─ 需要独立空间但不离开上下文 → Drawer (侧滑,有 backdrop) +├─ 需要全屏焦点、强制决策 → Modal (居中,阻塞) +└─ 一次性状态通知 → Toast (自动消失) +``` + +**不要为可逆操作弹 confirm modal。** Toast + 撤销 > 确认对话框。 + +--- + +## 3. Microcopy 设计原则 + +### 3.1 核心规则 + +1. **不泄露抽象**:用户面对的文字永远不包含内部文件名、变量名或技术概念。用户说"密钥配置"而非"mykey.py",说"本地仓库"而非"GA 目录"。 +2. **动词优先**:操作按钮用动词(导入 / 导出 / 连接 / 断开),不用名词。 +3. **错误消息说后果,不说原因**:用户关心"能不能继续"而非"哪行代码报错"。技术细节属于 L3(日志/开发者工具),不上 Toast。 + +### 3.2 错误消息分类学 + +| 类型 | 用户体验 | 消息风格 | 实现方式 | +|------|---------|---------|---------| +| 验证失败 | 即时反馈,秒级 | 告诉用户哪里不对 + 怎么修 | Toast error | +| 操作超时 | 等待后失败 | 告诉用户"环境可能不完整" | Toast error | +| 静默回退 | 启动时发现问题 | 告诉用户"已自动处理" | Toast info(一次性) | +| 网络断开 | 持续状态 | 状态指示器变灰 + 重连中动画 | UI 状态变更 | + +### 3.3 Toast vs Modal vs Inline + +| 条件 | 选择 | +|------|------| +| 操作成功/失败通知,不需要用户做选择 | Toast | +| 不可逆破坏性操作(删除数据、断开连接后数据丢失) | Confirm Modal | +| 可逆操作的失败 | Toast error(不带 retry 按钮,用户自然会重试) | +| 持续性状态(连接中 / 离线) | Statusbar 指示器或 inline badge | +| 表单校验错误 | Inline(字段下方红字) | + +### 3.4 i18n Key 命名 + +格式:`domain.camelCaseKey` + +| 类型 | 命名模式 | 示例 | +|------|---------|------| +| 标签 | `domain.nounPhrase` | `data.importKey`, `nav.chat` | +| 按钮 | `domain.verbBtn` 或直接动词 | `data.importKeyBtn` | +| Tooltip | `domain.keyTip` | `data.importKeyTip` | +| 成功 | `domain.keySuccess` | `data.importKeySuccess` | +| 错误 | `domain.keyError` 或 `err.specificCase` | `data.localRepoErrTimeout` | + +详细 microcopy 对照表见 `spec/microcopy.md`。 + +--- + +## 4. 视觉语言 + +### 4.1 颜色系统 + +双层 token 架构: + +**语义层**(Desktop 自定义语义 token): +- `--foreground` / `--background` — 全局前景/背景 +- `--ui-text-secondary` / `tertiary` / `quaternary` — 文本权重递减 +- `--ui-icon-nav` / `--ui-icon-nav-active` — 导航图标 +- `--ui-row-hover-background` / `--ui-control-hover-background` — 交互反馈 +- `--ui-accent` / `--theme-primary` — 强调色 (hsl 220) +- `--ui-chat-surface-background` / `--ui-chat-bubble-background` — 对话区域 +- `--ui-stroke-tertiary` / `secondary` — 边框 + +**组件层**(Semi UI 框架提供): +- `--semi-color-text-0/1/2/3` — 正文层级 +- `--semi-color-fill-0/1/2` — 填充层级 +- `--semi-color-primary` / `danger` / `warning` — 语义色 +- `--semi-color-border` — 默认边框 + +**规则**:自定义 token 用于 shell(nav、sidebar、statusbar、thread);Semi token 用于 Semi 组件内部和通用 UI 元素。两套体系通过主题切换同步。 + +### 4.2 暗色/亮色模式 + +- 切换机制:`` + `` +- 不跟随系统——用户手动选择 +- 所有自定义 token 在 `[data-appearance="dark"]` 下重新定义 +- 设计新组件时必须在两种模式下验证 + +### 4.3 排版 + +| 用途 | 字体 | 变量 | +|------|------|------| +| 正文/UI | 系统 sans-serif(SF Pro → PingFang → Microsoft YaHei) | `--font-sans` | +| 代码/模型名 | JetBrains Mono → 系统 monospace | `--font-mono` | +| 对话文本 | 同正文,但尺寸用户可调(10–20px) | `--chat-font` | +| 工具调用/标注 | `--conversation-tool-font-size: 0.6875rem` | — | + +**规则**:模型名称、profile 名称、技术标识符永远使用 `--font-mono`。 + +### 4.4 图标 + +| 来源 | 用途 | 加载方式 | +|------|------|---------| +| VS Code Codicons | 导航、通用操作(展开/收起/复制/搜索) | `@font-face` 字体 | +| Semi Icons | 表单元素、Semi 组件内置图标 | React 组件 | + +不引入第三方图标库。Codicon 覆盖不到的场景优先用 Semi Icons,其次用 inline SVG。 + +### 4.5 动效原则 + +1. **过渡优先**:所有状态变化用 `transition`(100–150ms ease-out),不用 `@keyframes` 动画 +2. **唯一例外**:thinking shimmer(呼吸灯效果)和 loading spinner +3. **透明度渐变 = "可选关注"**:非核心信息默认 0.67 opacity,hover → 1.0 +4. **禁止弹跳/overshoot**:Desktop 应用追求稳定感,不是 playful + +### 4.6 布局适配 + +- 无 media query breakpoint(这是桌面应用,不是响应式网页) +- 宽度约束用 `min()` + CSS 变量上限(`--composer-width: 780px`) +- 高度填充用 `flex: 1; min-height: 0`,每一层都声明 +- 溢出统一处理:`min-width: 0` on flex children + `text-overflow: ellipsis` +- 平台几何差异通过 `data-platform="macos|windows"` 选择器处理 + +--- + +## 5. 组件使用规范 + +### 5.1 Semi UI + +直接使用,不做封装层: +```tsx +import { Button, Modal, Tooltip } from '@douyinfe/semi-ui'; +``` + +不创建 `` 之类的 wrapper。如果需要统一样式,用 CSS class override(`.ga-` 前缀)。 + +### 5.2 自定义组件的 CSS 命名 + +- Class 命名:`ga-{domain}-{element}` BEM 风格(如 `.ga-nav-btn`、`.ga-data-row-label`) +- Thread 系统:使用 `data-slot` 属性选择器(如 `[data-slot="user-bubble"]`)——slot-based 选择器模型,保证组件可组合而不依赖 DOM 层级 +- 状态表示:`data-*` 属性(`data-clamped`、`data-expanded`、`data-following`) + +### 5.3 通知系统 + +使用自定义 store + portal(`NotificationStack`),不使用 Semi 的 `Notification` 组件: +- Error/Warning → 顶部居中,有关闭按钮,不自动消失 +- Info/Success → 右下角,自动消失(3s) + +Semi `Toast` 仍用于简单的一次性操作反馈(import/export 结果)。 + +### 5.4 表单 + +不使用表单库。手动构建: +- 行级容器:`.ga-form-field` +- 标签:`.ga-form-label` +- 高级折叠:`.ga-form-advanced-toggle` +- 输入组件直接使用 Semi 的 `Input` / `InputNumber` / `RadioGroup` + +### 5.5 平台适配 + +```css +:root[data-platform="macos"] { --ga-titlebar-height: 38px; } +:root[data-platform="windows"] .ga-titlebar-controls { left: 8px; } +``` + +- macOS:系统流量灯占据左上角 72px 宽度,控制按钮放在流量灯右侧 +- Windows:自绘标题栏,控制按钮放在左侧顶部 +- 拖拽区域通过事件委托实现(`useDragWindow()` 检查 `y < 38px`) + +--- + +## 附录:相关文档索引 + +| 文档 | 位置 | 内容 | +|------|------|------| +| Microcopy 对照表 | `spec/microcopy.md` | 数据维护区域的完整三层文案 + 错误路由 | +| 本地仓库连接契约 | `spec/local-repo-connection.md` | 连接流程状态机 + Rust 命令签名 + 验证规则 | +| 记忆导入设计 | `spec/memory-import.md` | 导入流程 + 后端 API 契约 | +| 模型选择契约 | `spec/model-selection.md` | Model Pill 行为 + 前后端分离协议 + Conductor 解耦 | diff --git a/frontends/desktop/e2e/README.md b/frontends/desktop/e2e/README.md new file mode 100644 index 000000000..719d33128 --- /dev/null +++ b/frontends/desktop/e2e/README.md @@ -0,0 +1,49 @@ +# GenericAgent Desktop E2E Harness + +The desktop harness drives both Chrome and the real Tauri application with WebdriverIO. Both modes use the same deterministic fake OpenAI server, semantic page objects, isolated product sandbox, and token assertions. + +## Commands + +Run from `frontends/desktop` after `npm ci`: + +```bash +npm run e2e:doctor +npm run e2e:browser +npm run e2e:desktop +npm run e2e:desktop:full +``` + +`e2e:browser` is the fast PR journey. It sends messages through the real UI and bridge, verifies exact per-call usage, corrupt-tail tolerance, restart persistence, localized empty replies, and a hard crash while the second model call is in flight. + +`e2e:desktop` builds the application with Cargo's `e2e` feature and `src-tauri/tauri.e2e.conf.json`, then drives the native window. It verifies sandbox identity, chat, usage, bridge-offline UI, and recovery through the real Tauri `start_bridge` command. Linux needs a display; CI uses `xvfb-run -a`. + +`e2e:desktop:full` additionally places an identified foreign listener on the isolated bridge port, verifies that the native launcher refuses to take it over, then releases the port and retries recovery through the UI. It is reserved for nightly/manual runs. + +The real-model canary is intentionally separate from merge gates: + +```bash +GA_E2E_CANARY_BASE=https://provider.example \ +GA_E2E_CANARY_KEY=... \ +GA_E2E_CANARY_MODEL=low-cost-model \ +npm run e2e:canary +``` + +## Isolation and safety + +Every run creates a sentinel-protected temporary root and copies the current tracked and untracked product files, excluding developer metadata and build outputs. The harness creates isolated HOME/settings, `mykey.py`, sessions, uploads, ledger, reports, dynamic ports, and a random control token. The bridge identity must resolve to that exact root before a browser journey can continue. + +PR runs reject non-loopback fake-model URLs. Model credentials are dummy values. Control routes require `GA_E2E=1`, a random `X-GA-E2E-Token`, and a loopback peer. They do not exist in normal bridge startup. + +The WDIO Rust plugins are optional Cargo dependencies enabled only by the `e2e` feature. The frontend plugin is included only when `VITE_GA_E2E=1`. `npm run test:e2e-isolation` builds production assets and fails if WDIO markers remain. + +## Failure evidence + +Failed runs retain their sentinel sandbox and copy redacted reports to `frontends/desktop/e2e-results/`. Evidence includes screenshots, page source, bridge/Vite logs, bootstrap snapshots, endpoint snapshots, fake-model request timing, ledger data, PIDs, and ports. Authorization and key-bearing log lines are replaced before writing. + +Set `GA_E2E_PYTHON` when automatic Python discovery cannot find a runtime with `aiohttp`. Set `GA_E2E_ARTIFACT_DIR` to change the stable report-copy destination. `GA_E2E_APPLICATION` may point to an already-built binary only when that binary was compiled with the same `VITE_BRIDGE_BASE`; normal use should let `e2e:desktop` build it after allocating ports. + +## CI topology + +- Pull requests: TypeScript, Vitest, Python ledger/process contracts, Rust production/E2E feature tests, production isolation, browser E2E, and Linux native Tauri smoke. +- Nightly/manual: native Tauri on Windows, Linux, and macOS, plus the credentialed real-model protocol canary when dedicated secrets are configured. +- The existing Windows portable-package journey remains under `e2e/windows/` and continues to validate packaged first-run and port-conflict behavior. diff --git a/frontends/desktop/e2e/canary.ts b/frontends/desktop/e2e/canary.ts new file mode 100644 index 000000000..e43b703ef --- /dev/null +++ b/frontends/desktop/e2e/canary.ts @@ -0,0 +1,28 @@ +const base = process.env.GA_E2E_CANARY_BASE; +const key = process.env.GA_E2E_CANARY_KEY; +const model = process.env.GA_E2E_CANARY_MODEL; + +if (!base || !key || !model) { + throw new Error('Canary is manual/nightly only; set GA_E2E_CANARY_BASE, GA_E2E_CANARY_KEY, and GA_E2E_CANARY_MODEL'); +} +const url = new URL('/v1/chat/completions', base); +const response = await fetch(url, { + method: 'POST', + headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json' }, + body: JSON.stringify({ + model, + stream: false, + max_tokens: 16, + messages: [{ role: 'user', content: 'Reply with OK.' }], + }), +}); +if (!response.ok) throw new Error(`Canary protocol failed: HTTP ${response.status}`); +const body = await response.json() as { + choices?: Array<{ message?: { content?: string } }>; + usage?: { prompt_tokens?: number; completion_tokens?: number }; +}; +if (!body.choices?.[0]?.message?.content) throw new Error('Canary returned no assistant content'); +if ((body.usage?.prompt_tokens || 0) + (body.usage?.completion_tokens || 0) <= 0) { + throw new Error('Canary returned no token usage'); +} +process.stdout.write('Canary protocol, assistant content, and usage checks passed.\n'); diff --git a/frontends/desktop/e2e/doctor.ts b/frontends/desktop/e2e/doctor.ts new file mode 100644 index 000000000..d983fee7a --- /dev/null +++ b/frontends/desktop/e2e/doctor.ts @@ -0,0 +1,39 @@ +import { existsSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { resolve } from 'node:path'; + +interface Check { name: string; ok: boolean; detail: string } + +function executable(name: string, args = ['--version']): Check { + const result = spawnSync(name, args, { encoding: 'utf8' }); + const output = `${result.stdout || ''}${result.stderr || ''}`.trim().split(/\r?\n/)[0] || 'not found'; + return { name, ok: result.status === 0, detail: output }; +} + +const root = resolve(process.cwd()); +const repoRoot = resolve(root, '..', '..'); +const worktreeOutput = spawnSync('git', ['worktree', 'list', '--porcelain'], { cwd: repoRoot, encoding: 'utf8' }).stdout; +const worktrees = worktreeOutput.split(/\r?\n/) + .filter((line) => line.startsWith('worktree ')) + .map((line) => line.slice(9)); +const pythonCandidates = process.platform === 'win32' + ? [repoRoot, ...worktrees].map((path) => resolve(path, '.venv', 'Scripts', 'python.exe')).concat('python') + : [repoRoot, ...worktrees].map((path) => resolve(path, '.venv', 'bin', 'python')).concat('python3', 'python'); +const python = pythonCandidates.find((candidate) => + (!candidate.includes('/') || existsSync(candidate)) + && spawnSync(candidate, ['-c', 'import aiohttp'], { stdio: 'ignore' }).status === 0, +); +const checks: Check[] = [ + executable(process.execPath, ['--version']), + python + ? executable(python, ['-c', 'import aiohttp; print("aiohttp available")']) + : { name: 'Python bridge runtime', ok: false, detail: 'set GA_E2E_PYTHON to a Python with aiohttp' }, + executable('cargo'), + { name: 'WDIO service', ok: existsSync(resolve(root, 'node_modules/@wdio/tauri-service')), detail: 'node_modules/@wdio/tauri-service' }, + { name: 'Tauri config', ok: existsSync(resolve(root, 'src-tauri/tauri.e2e.conf.json')), detail: 'src-tauri/tauri.e2e.conf.json' }, +]; + +for (const check of checks) { + process.stdout.write(`${check.ok ? 'OK ' : 'FAIL '} ${check.name}: ${check.detail}\n`); +} +if (checks.some((check) => !check.ok)) process.exitCode = 1; diff --git a/frontends/desktop/e2e/harness/context.ts b/frontends/desktop/e2e/harness/context.ts new file mode 100644 index 000000000..e97288a00 --- /dev/null +++ b/frontends/desktop/e2e/harness/context.ts @@ -0,0 +1,33 @@ +import { readFileSync } from 'node:fs'; + +export interface E2EContextFile { + mode: 'browser' | 'desktop'; + sandboxRoot: string; + reports: string; + bridgeBase: string; + viteUrl?: string; + controlBase: string; + controlToken: string; + application?: string; + appEnv?: Record; +} + +export function loadE2EContext(): E2EContextFile { + const path = process.env.GA_E2E_CONTEXT_FILE; + if (!path) throw new Error('GA_E2E_CONTEXT_FILE is not set; use an e2e:* npm command'); + return JSON.parse(readFileSync(path, 'utf8')) as E2EContextFile; +} + +export async function controlRequest(path: string, init: RequestInit = {}): Promise { + const context = loadE2EContext(); + const response = await fetch(`${context.controlBase}${path}`, { + ...init, + headers: { + 'content-type': 'application/json', + 'x-ga-e2e-token': context.controlToken, + ...init.headers, + }, + }); + if (!response.ok) throw new Error(`Harness control ${path} failed: HTTP ${response.status} ${await response.text()}`); + return await response.json() as T; +} diff --git a/frontends/desktop/e2e/harness/fake-openai.test.ts b/frontends/desktop/e2e/harness/fake-openai.test.ts new file mode 100644 index 000000000..529437680 --- /dev/null +++ b/frontends/desktop/e2e/harness/fake-openai.test.ts @@ -0,0 +1,71 @@ +// @vitest-environment node +import { afterEach, describe, expect, it } from 'vitest'; +import { FakeOpenAI } from './fake-openai'; + +const servers: FakeOpenAI[] = []; + +afterEach(async () => { + await Promise.all(servers.splice(0).map((server) => server.stop())); +}); + +describe('FakeOpenAI', () => { + it('streams deterministic text and exact usage while redacting authorization', async () => { + const server = new FakeOpenAI(); + servers.push(server); + const baseUrl = await server.start(); + + const response = await fetch(`${baseUrl}/v1/chat/completions`, { + method: 'POST', + headers: { authorization: 'Bearer must-not-leak', 'content-type': 'application/json' }, + body: JSON.stringify({ model: 'e2e-model', stream: true, messages: [{ role: 'user', content: 'hello' }] }), + }); + const body = await response.text(); + + expect(response.status).toBe(200); + expect(body).toContain('Harness reply'); + expect(body).toContain('"prompt_tokens":101'); + expect(body).toContain('"completion_tokens":17'); + expect(server.transcript()).toEqual([ + expect.objectContaining({ path: '/v1/chat/completions', scenario: 'normal', authorization: '[redacted]' }), + ]); + }); + + it('can fail or disconnect deterministically from prompt markers', async () => { + const server = new FakeOpenAI(); + servers.push(server); + const baseUrl = await server.start(); + + const failed = await fetch(`${baseUrl}/v1/chat/completions`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ messages: [{ role: 'user', content: '[E2E:http500]' }] }), + }); + expect(failed.status).toBe(500); + + await expect(fetch(`${baseUrl}/v1/chat/completions`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ messages: [{ role: 'user', content: '[E2E:disconnect]' }] }), + })).rejects.toThrow(); + }); + + it('waits for calls from the requested scenario instead of all earlier traffic', async () => { + const server = new FakeOpenAI(); + servers.push(server); + const baseUrl = await server.start(); + + await fetch(`${baseUrl}/v1/chat/completions`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ messages: [{ role: 'user', content: 'ordinary request' }] }), + }); + await expect(server.waitForScenarioRequests('two-call-hang', 1, 25)).rejects.toThrow(/two-call-hang/); + + await fetch(`${baseUrl}/v1/chat/completions`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ messages: [{ role: 'user', content: '[E2E:two-call-hang]' }] }), + }); + await expect(server.waitForScenarioRequests('two-call-hang', 1, 25)).resolves.toBeUndefined(); + }); +}); diff --git a/frontends/desktop/e2e/harness/fake-openai.ts b/frontends/desktop/e2e/harness/fake-openai.ts new file mode 100644 index 000000000..b6268758a --- /dev/null +++ b/frontends/desktop/e2e/harness/fake-openai.ts @@ -0,0 +1,137 @@ +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'; +import type { AddressInfo } from 'node:net'; + +export type FakeScenario = 'normal' | 'http500' | 'disconnect' | 'two-call-hang'; + +export interface FakeTranscriptEntry { + path: string; + scenario: FakeScenario; + authorization: '[redacted]' | ''; + model: string; + call: number; +} + +export class FakeOpenAI { + private server: Server | null = null; + private entries: FakeTranscriptEntry[] = []; + private scenarioCalls = new Map(); + private heldResponses = new Set(); + + async start(): Promise { + if (this.server) throw new Error('FakeOpenAI already started'); + this.server = createServer((request, response) => void this.handle(request, response)); + await new Promise((resolve, reject) => { + this.server!.once('error', reject); + this.server!.listen(0, '127.0.0.1', resolve); + }); + const { port } = this.server.address() as AddressInfo; + return `http://127.0.0.1:${port}`; + } + + async stop(): Promise { + for (const response of this.heldResponses) response.destroy(); + this.heldResponses.clear(); + const server = this.server; + this.server = null; + if (!server) return; + await new Promise((resolve) => server.close(() => resolve())); + } + + transcript(): FakeTranscriptEntry[] { + return this.entries.map((entry) => ({ ...entry })); + } + + releaseHeld(): void { + for (const response of this.heldResponses) this.writeTextResponse(response, 'Harness resumed'); + this.heldResponses.clear(); + } + + async waitForScenarioRequests(scenario: FakeScenario, count: number, timeoutMs = 10_000): Promise { + const deadline = Date.now() + timeoutMs; + while (this.entries.filter((entry) => entry.scenario === scenario).length < count) { + if (Date.now() >= deadline) { + throw new Error(`Timed out waiting for ${count} ${scenario} fake LLM requests`); + } + await new Promise((resolve) => setTimeout(resolve, 25)); + } + } + + private async handle(request: IncomingMessage, response: ServerResponse): Promise { + const chunks: Buffer[] = []; + for await (const chunk of request) chunks.push(Buffer.from(chunk)); + let body: Record = {}; + try { body = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { /* invalid body handled as normal */ } + const scenario = this.scenario(body); + const call = (this.scenarioCalls.get(scenario) ?? 0) + 1; + this.scenarioCalls.set(scenario, call); + this.entries.push({ + path: request.url || '', + scenario, + authorization: request.headers.authorization ? '[redacted]' : '', + model: typeof body.model === 'string' ? body.model : '', + call, + }); + + if (scenario === 'http500') { + response.writeHead(500, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ error: { message: 'deterministic harness failure' } })); + return; + } + if (scenario === 'disconnect') { + request.socket.destroy(); + return; + } + if (scenario === 'two-call-hang' && call > 1) { + this.heldResponses.add(response); + response.on('close', () => this.heldResponses.delete(response)); + return; + } + if (scenario === 'two-call-hang') { + this.writeToolResponse(response); + return; + } + this.writeTextResponse(response, 'Harness reply'); + } + + private scenario(body: Record): FakeScenario { + const raw = JSON.stringify(body); + if (raw.includes('[E2E:http500]')) return 'http500'; + if (raw.includes('[E2E:disconnect]')) return 'disconnect'; + if (raw.includes('[E2E:two-call-hang]')) return 'two-call-hang'; + return 'normal'; + } + + private headers(response: ServerResponse): void { + response.writeHead(200, { + 'content-type': 'text/event-stream; charset=utf-8', + 'cache-control': 'no-cache', + connection: 'close', + }); + } + + private writeTextResponse(response: ServerResponse, text: string): void { + this.headers(response); + response.write(`data: ${JSON.stringify({ choices: [{ delta: { content: text } }] })}\n\n`); + response.write(`data: ${JSON.stringify({ choices: [], usage: { + prompt_tokens: 101, + completion_tokens: 17, + prompt_tokens_details: { cached_tokens: 11 }, + } })}\n\n`); + response.end('data: [DONE]\n\n'); + } + + private writeToolResponse(response: ServerResponse): void { + this.headers(response); + response.write(`data: ${JSON.stringify({ choices: [{ delta: { tool_calls: [{ + index: 0, + id: 'call_harness_1', + function: { name: 'update_working_checkpoint', arguments: '{"key_info":"e2e"}' }, + }] } }] })}\n\n`); + response.write(`data: ${JSON.stringify({ choices: [], usage: { + prompt_tokens: 101, + completion_tokens: 17, + prompt_tokens_details: { cached_tokens: 11 }, + } })}\n\n`); + response.end('data: [DONE]\n\n'); + } +} diff --git a/frontends/desktop/e2e/harness/orchestrator.ts b/frontends/desktop/e2e/harness/orchestrator.ts new file mode 100644 index 000000000..2dbc22ccf --- /dev/null +++ b/frontends/desktop/e2e/harness/orchestrator.ts @@ -0,0 +1,394 @@ +import { randomUUID } from 'node:crypto'; +import { spawn, spawnSync, type ChildProcess } from 'node:child_process'; +import { appendFile, cp, mkdir, readFile, writeFile } from 'node:fs/promises'; +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { basename, dirname, join, resolve } from 'node:path'; +import { FakeOpenAI } from './fake-openai'; +import { allocateLoopbackPort, pathsReferToSameEntry, redactEvidence } from './runtime'; +import { cleanupSandbox, createSandbox, type SandboxLayout } from './sandbox'; +import type { E2EContextFile } from './context'; + +type HarnessMode = 'browser' | 'desktop'; + +interface StartOptions { + mode: HarnessMode; + desktopRoot: string; + pythonPath: string; + application?: string; +} + +interface ManagedProcess { + name: string; + child: ChildProcess; +} + +function inheritedEnv(env: Record): NodeJS.ProcessEnv { + return Object.fromEntries(Object.entries(env)); +} + +async function waitForHttp(url: string, timeoutMs = 30_000): Promise { + const deadline = Date.now() + timeoutMs; + let lastError = 'not started'; + while (Date.now() < deadline) { + try { + const response = await fetch(url); + if (response.ok) return response; + lastError = `HTTP ${response.status}`; + } catch (error) { + lastError = String(error); + } + await new Promise((resolveWait) => setTimeout(resolveWait, 100)); + } + throw new Error(`Timed out waiting for ${url}: ${lastError}`); +} + +async function stopChild(processInfo: ManagedProcess | null, hard = false): Promise { + if (!processInfo || processInfo.child.exitCode !== null) return; + const child = processInfo.child; + if (process.platform === 'win32' && child.pid) { + spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore' }); + return; + } + child.kill(hard ? 'SIGKILL' : 'SIGTERM'); + await Promise.race([ + new Promise((resolveExit) => child.once('exit', () => resolveExit())), + new Promise((resolveWait) => setTimeout(resolveWait, 3_000)), + ]); + if (child.exitCode === null) child.kill('SIGKILL'); +} + +async function jsonBody(request: IncomingMessage): Promise> { + const chunks: Buffer[] = []; + for await (const chunk of request) chunks.push(Buffer.from(chunk)); + if (!chunks.length) return {}; + return JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record; +} + +function jsonResponse(response: ServerResponse, status: number, body: unknown): void { + response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' }); + response.end(JSON.stringify(body)); +} + +export class DesktopE2EHarness { + private readonly options: StartOptions; + private fake = new FakeOpenAI(); + private sandbox: SandboxLayout | null = null; + private bridge: ManagedProcess | null = null; + private vite: ManagedProcess | null = null; + private controlServer: Server | null = null; + private foreignBridge: Server | null = null; + private contextPath = ''; + private failed = false; + private bridgePort = 0; + private vitePort = 0; + private controlToken = randomUUID(); + private fakeBase = ''; + + constructor(options: StartOptions) { + this.options = options; + } + + get contextFile(): string { return this.contextPath; } + + async start(): Promise { + const desktopRoot = resolve(this.options.desktopRoot); + const repoRoot = resolve(desktopRoot, '..', '..'); + this.bridgePort = await allocateLoopbackPort(); + this.vitePort = await allocateLoopbackPort(); + this.fakeBase = await this.fake.start(); + this.sandbox = await createSandbox({ + repoRoot, + pythonPath: this.options.pythonPath, + fakeBaseUrl: this.fakeBase, + bridgePort: this.bridgePort, + vitePort: this.vitePort, + controlToken: this.controlToken, + }); + + if (this.options.mode === 'browser') { + await this.seedLegacyHistory(); + await this.startBridge(); + await this.startVite(desktopRoot); + } + const controlBase = await this.startControlServer(); + const context: E2EContextFile = { + mode: this.options.mode, + sandboxRoot: this.sandbox.root, + reports: this.sandbox.reports, + bridgeBase: `http://127.0.0.1:${this.bridgePort}`, + viteUrl: this.options.mode === 'browser' ? `http://127.0.0.1:${this.vitePort}` : undefined, + controlBase, + controlToken: this.controlToken, + application: this.options.application, + appEnv: this.options.mode === 'desktop' ? this.sandbox.env : undefined, + }; + this.contextPath = join(this.sandbox.root, '.e2e-context.json'); + await writeFile(this.contextPath, JSON.stringify(context, null, 2), 'utf8'); + await this.captureSnapshot('started'); + return context; + } + + markFailed(): void { this.failed = true; } + + async stop(): Promise { + await this.captureSnapshot(this.failed ? 'failed' : 'completed').catch(() => {}); + await stopChild(this.vite); + await stopChild(this.bridge); + await this.releaseForeignBridge(); + if (this.options.mode === 'desktop') await this.stopExternalBridge().catch(() => {}); + this.fake.releaseHeld(); + await this.fake.stop(); + if (this.controlServer) { + await new Promise((resolveClose) => this.controlServer!.close(() => resolveClose())); + this.controlServer = null; + } + const sandbox = this.sandbox; + this.sandbox = null; + if (sandbox && !this.failed) { + await cleanupSandbox(sandbox.root); + } else if (sandbox) { + const artifactRoot = resolve( + process.env.GA_E2E_ARTIFACT_DIR || join(this.options.desktopRoot, 'e2e-results'), + `${basename(sandbox.root)}-${Date.now()}`, + ); + await mkdir(dirname(artifactRoot), { recursive: true }); + await cp(sandbox.reports, artifactRoot, { recursive: true }); + process.stderr.write(`E2E report copied to ${artifactRoot}\n`); + process.stderr.write(`E2E failure evidence preserved at ${sandbox.root}\n`); + } + } + + private async startBridge(): Promise { + const sandbox = this.requireSandbox(); + const script = join(sandbox.root, 'frontends', 'desktop_bridge.py'); + this.bridge = this.spawnLogged('bridge', this.options.pythonPath, [script], sandbox.root, sandbox.env); + const identityResponse = await waitForHttp(`http://127.0.0.1:${this.bridgePort}/services/identity`); + const identity = await identityResponse.json() as { ga_root?: string }; + if (!pathsReferToSameEntry(String(identity.ga_root || ''), sandbox.root)) { + throw new Error(`Bridge escaped E2E sandbox: ${identity.ga_root || ''}`); + } + } + + private async seedLegacyHistory(): Promise { + const sandbox = this.requireSandbox(); + const temp = join(sandbox.root, 'temp'); + await mkdir(temp, { recursive: true }); + await writeFile(join(temp, 'desktop_token_history.json'), JSON.stringify({ + history: [{ + sessionId: 'legacy-e2e', + title: 'Legacy E2E', + model: 'legacy-model', + ts: 1_700_000_000, + }], + snap: { + 'GA-legacy-e2e': { input: 2, output: 3, cacheCreate: 0, cacheRead: 1 }, + }, + }), 'utf8'); + } + + private async restartBridge(hard = false): Promise { + await stopChild(this.bridge, hard); + this.bridge = null; + await this.startBridge(); + } + + private async startVite(desktopRoot: string): Promise { + const sandbox = this.requireSandbox(); + const viteBin = join(desktopRoot, 'node_modules', 'vite', 'bin', 'vite.js'); + this.vite = this.spawnLogged( + 'vite', + process.execPath, + [viteBin, '--host', '127.0.0.1', '--port', String(this.vitePort), '--strictPort'], + desktopRoot, + sandbox.env, + ); + await waitForHttp(`http://127.0.0.1:${this.vitePort}`); + } + + private spawnLogged(name: string, executable: string, args: string[], cwd: string, env: Record): ManagedProcess { + const sandbox = this.requireSandbox(); + const child = spawn(executable, args, { + cwd, + env: inheritedEnv(env), + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }); + const logPath = join(sandbox.reports, `${name}.log`); + const capture = (chunk: Buffer) => void appendFile(logPath, redactEvidence(chunk.toString('utf8')), 'utf8'); + child.stdout?.on('data', capture); + child.stderr?.on('data', capture); + child.once('error', (error) => void appendFile(logPath, `\n[spawn error] ${error}\n`, 'utf8')); + return { name, child }; + } + + private async startControlServer(): Promise { + this.controlServer = createServer((request, response) => void this.handleControl(request, response)); + await new Promise((resolveListen, reject) => { + this.controlServer!.once('error', reject); + this.controlServer!.listen(0, '127.0.0.1', resolveListen); + }); + const { port } = this.controlServer.address() as AddressInfo; + return `http://127.0.0.1:${port}`; + } + + private async handleControl(request: IncomingMessage, response: ServerResponse): Promise { + try { + if (request.socket.remoteAddress !== '127.0.0.1' && request.socket.remoteAddress !== '::ffff:127.0.0.1') { + jsonResponse(response, 403, { error: 'loopback only' }); return; + } + if (request.headers['x-ga-e2e-token'] !== this.controlToken) { + jsonResponse(response, 403, { error: 'forbidden' }); return; + } + const url = new URL(request.url || '/', 'http://127.0.0.1'); + if (request.method === 'GET' && url.pathname === '/fake/transcript') { + jsonResponse(response, 200, { requests: this.fake.transcript() }); return; + } + if (request.method === 'POST' && url.pathname === '/bridge/restart') { + await this.restartBridge(false); + jsonResponse(response, 200, { ok: true }); return; + } + if (request.method === 'POST' && url.pathname === '/bridge/crash-after-second-call') { + await this.fake.waitForScenarioRequests('two-call-hang', 2, 20_000); + await this.restartBridge(true); + jsonResponse(response, 200, { ok: true }); return; + } + if (request.method === 'POST' && url.pathname === '/bridge/kill-external') { + if (this.options.mode !== 'desktop') { + jsonResponse(response, 400, { error: 'desktop mode only' }); return; + } + await this.stopExternalBridge(); + jsonResponse(response, 200, { ok: true }); return; + } + if (request.method === 'POST' && url.pathname === '/port/occupy-bridge') { + if (this.options.mode !== 'desktop') { + jsonResponse(response, 400, { error: 'desktop mode only' }); return; + } + await this.stopExternalBridge(); + await this.occupyBridgePort(); + jsonResponse(response, 200, { ok: true }); return; + } + if (request.method === 'POST' && url.pathname === '/port/release-bridge') { + await this.releaseForeignBridge(); + jsonResponse(response, 200, { ok: true }); return; + } + if (request.method === 'POST' && url.pathname === '/ledger/corrupt-tail') { + const ledger = join(this.requireSandbox().root, 'temp', 'token_ledger.jsonl'); + await appendFile(ledger, '{"truncated":\nnot-json\n', 'utf8'); + jsonResponse(response, 200, { ok: true }); return; + } + if (request.method === 'POST' && url.pathname === '/settings/language') { + const body = await jsonBody(request); + const lang = body.lang === 'en' ? 'en' : body.lang === 'zh' ? 'zh' : null; + if (!lang) { jsonResponse(response, 400, { error: 'lang must be zh or en' }); return; } + const settingsPath = join(this.requireSandbox().home, '.ga_desktop_settings.json'); + const settings = JSON.parse(await readFile(settingsPath, 'utf8')) as Record; + settings.lang = lang; + await writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf8'); + await this.restartBridge(false); + jsonResponse(response, 200, { ok: true, lang }); return; + } + if (request.method === 'POST' && url.pathname === '/snapshot') { + await this.captureSnapshot('requested'); + jsonResponse(response, 200, { ok: true }); return; + } + jsonResponse(response, 404, { error: 'not found' }); + } catch (error) { + jsonResponse(response, 500, { error: String(error) }); + } + } + + private async captureSnapshot(name: string): Promise { + const sandbox = this.requireSandbox(); + await mkdir(sandbox.reports, { recursive: true }); + const endpoints: Record = {}; + for (const path of ['/services/identity', '/sessions', '/token-history']) { + try { + const response = await fetch(`http://127.0.0.1:${this.bridgePort}${path}`); + endpoints[path] = await response.json(); + } catch (error) { + endpoints[path] = { error: String(error) }; + } + } + const ledgerPath = join(sandbox.root, 'temp', 'token_ledger.jsonl'); + let ledger = ''; + try { ledger = await readFile(ledgerPath, 'utf8'); } catch { /* no calls yet */ } + await writeFile(join(sandbox.reports, `${name}-snapshot.json`), JSON.stringify({ + time: new Date().toISOString(), + sandbox: sandbox.root, + processes: { + bridge: this.bridge?.child.pid ?? null, + vite: this.vite?.child.pid ?? null, + }, + ports: { bridge: this.bridgePort, vite: this.vitePort }, + endpoints, + fakeRequests: this.fake.transcript(), + }, null, 2), 'utf8'); + if (ledger) await writeFile(join(sandbox.reports, `${name}-ledger.jsonl`), ledger, 'utf8'); + try { + await cp( + join(sandbox.root, 'temp', 'desktop_sessions'), + join(sandbox.reports, `${name}-sessions`), + { recursive: true }, + ); + } catch { /* no persisted sessions yet */ } + } + + private async stopExternalBridge(): Promise { + const sandbox = this.requireSandbox(); + let identity: { ga_root?: string; pid?: number }; + try { + identity = await (await fetch(`http://127.0.0.1:${this.bridgePort}/services/identity`)).json() as typeof identity; + } catch { + return; + } + if (!identity.pid) return; + if (!pathsReferToSameEntry(String(identity.ga_root || ''), sandbox.root)) { + throw new Error(`Refusing to stop bridge outside sandbox: ${identity.ga_root}`); + } + if (process.platform === 'win32') { + spawnSync('taskkill', ['/PID', String(identity.pid), '/T', '/F'], { stdio: 'ignore' }); + } else { + process.kill(identity.pid, 'SIGKILL'); + } + } + + private async occupyBridgePort(): Promise { + await this.releaseForeignBridge(); + const deadline = Date.now() + 5_000; + while (Date.now() < deadline) { + const server = createServer((request, response) => { + if (request.url === '/services/identity') { + jsonResponse(response, 200, { service: 'foreign-e2e-listener' }); + } else { + jsonResponse(response, 503, { error: 'foreign listener' }); + } + }); + try { + await new Promise((resolveListen, reject) => { + server.once('error', reject); + server.listen(this.bridgePort, '127.0.0.1', resolveListen); + }); + this.foreignBridge = server; + return; + } catch (error) { + server.close(); + if ((error as NodeJS.ErrnoException).code !== 'EADDRINUSE') throw error; + await new Promise((resolveWait) => setTimeout(resolveWait, 100)); + } + } + throw new Error(`Timed out occupying bridge port ${this.bridgePort}`); + } + + private async releaseForeignBridge(): Promise { + const server = this.foreignBridge; + this.foreignBridge = null; + if (!server) return; + await new Promise((resolveClose) => server.close(() => resolveClose())); + } + + private requireSandbox(): SandboxLayout { + if (!this.sandbox) throw new Error('E2E sandbox has not started'); + return this.sandbox; + } +} diff --git a/frontends/desktop/e2e/harness/runtime.test.ts b/frontends/desktop/e2e/harness/runtime.test.ts new file mode 100644 index 000000000..f15e0a340 --- /dev/null +++ b/frontends/desktop/e2e/harness/runtime.test.ts @@ -0,0 +1,55 @@ +// @vitest-environment node +import { link, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { + allocateLoopbackPort, + assertLoopbackUrl, + assertSandboxRoot, + pathsReferToSameEntry, + redactEvidence, +} from './runtime'; + +describe('E2E harness runtime safety', () => { + it('accepts loopback services and rejects external endpoints', () => { + expect(assertLoopbackUrl('http://127.0.0.1:1234/v1')).toBe('http://127.0.0.1:1234/v1'); + expect(assertLoopbackUrl('http://localhost:1234')).toBe('http://localhost:1234'); + expect(() => assertLoopbackUrl('https://api.example.com/v1')).toThrow(/loopback/i); + }); + + it('allocates currently available non-zero ports', async () => { + const first = await allocateLoopbackPort(); + const second = await allocateLoopbackPort(); + expect(first).toBeGreaterThan(0); + expect(second).toBeGreaterThan(0); + expect(first).not.toBe(second); + }); + + it('requires the sandbox sentinel before destructive cleanup', async () => { + const root = await mkdtemp(join(tmpdir(), 'ga-e2e-runtime-')); + expect(() => assertSandboxRoot(root)).toThrow(/sentinel/i); + await writeFile(join(root, '.ga-e2e-sandbox'), 'v1\n'); + expect(assertSandboxRoot(root)).toBe(root); + expect(await readFile(join(root, '.ga-e2e-sandbox'), 'utf8')).toBe('v1\n'); + }); + + it('compares filesystem identity instead of path spelling', async () => { + const root = await mkdtemp(join(tmpdir(), 'ga-e2e-path-identity-')); + const original = join(root, 'long-name'); + const alias = join(root, 'SHORT~1'); + const sibling = join(root, 'sibling'); + await writeFile(original, 'sandbox'); + await link(original, alias); + await writeFile(sibling, 'different'); + + expect(pathsReferToSameEntry(original, alias)).toBe(true); + expect(pathsReferToSameEntry(original, sibling)).toBe(false); + }); + + it('redacts credentials without removing useful process evidence', () => { + expect(redactEvidence('Authorization: Bearer secret\npid=42\napikey=abc')).toBe( + '[redacted sensitive line]\npid=42\n[redacted sensitive line]', + ); + }); +}); diff --git a/frontends/desktop/e2e/harness/runtime.ts b/frontends/desktop/e2e/harness/runtime.ts new file mode 100644 index 000000000..66daa4800 --- /dev/null +++ b/frontends/desktop/e2e/harness/runtime.ts @@ -0,0 +1,69 @@ +import { existsSync, realpathSync, statSync } from 'node:fs'; +import { createServer } from 'node:net'; +import { resolve } from 'node:path'; + +const allocatedPorts = new Set(); + +export function assertLoopbackUrl(raw: string): string { + const url = new URL(raw); + const loopback = url.hostname === '127.0.0.1' || url.hostname === 'localhost' || url.hostname === '[::1]'; + if (!loopback || (url.protocol !== 'http:' && url.protocol !== 'https:')) { + throw new Error(`E2E services must use an HTTP(S) loopback URL: ${raw}`); + } + return raw; +} + +export async function allocateLoopbackPort(): Promise { + for (;;) { + const port = await new Promise((resolvePort, reject) => { + const server = createServer(); + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + const selected = typeof address === 'object' && address ? address.port : 0; + server.close((error) => error ? reject(error) : resolvePort(selected)); + }); + }); + if (port > 0 && !allocatedPorts.has(port)) { + allocatedPorts.add(port); + return port; + } + } +} + +export function assertSandboxRoot(root: string): string { + const absolute = resolve(root); + if (!existsSync(`${absolute}/.ga-e2e-sandbox`)) { + throw new Error(`Refusing sandbox operation without sentinel: ${absolute}`); + } + return absolute; +} + +export function pathsReferToSameEntry(left: string, right: string): boolean { + try { + const leftPath = realpathSync.native(resolve(left)); + const rightPath = realpathSync.native(resolve(right)); + const normalize = (path: string): string => process.platform === 'win32' ? path.toLowerCase() : path; + if (normalize(leftPath) === normalize(rightPath)) return true; + + // Windows can preserve an 8.3 spelling (RUNNER~1) for one caller while another reports + // the long path (runneradmin). Device + inode compare the directory identity without + // weakening the sandbox boundary to prefix or case-only checks. + const leftIdentity = statSync(leftPath, { bigint: true }); + const rightIdentity = statSync(rightPath, { bigint: true }); + return leftIdentity.ino !== 0n + && leftIdentity.dev === rightIdentity.dev + && leftIdentity.ino === rightIdentity.ino; + } catch { + return false; + } +} + +export function redactEvidence(text: string): string { + return text.split(/\r?\n/).map((line) => { + const lower = line.toLowerCase(); + return ['authorization', 'bearer', 'apikey', 'api_key', 'secret'].some((marker) => lower.includes(marker)) + ? '[redacted sensitive line]' + : line; + }).join('\n'); +} diff --git a/frontends/desktop/e2e/harness/sandbox.test.ts b/frontends/desktop/e2e/harness/sandbox.test.ts new file mode 100644 index 000000000..275342374 --- /dev/null +++ b/frontends/desktop/e2e/harness/sandbox.test.ts @@ -0,0 +1,51 @@ +// @vitest-environment node +import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { cleanupSandbox, createSandbox } from './sandbox'; + +describe('isolated GenericAgent sandbox', () => { + it('copies only selected workspace files and replaces credentials with loopback fake config', async () => { + const source = await mkdtemp(join(tmpdir(), 'ga-e2e-source-')); + await mkdir(join(source, 'frontends'), { recursive: true }); + await writeFile(join(source, 'agentmain.py'), '# current working tree\n'); + await writeFile(join(source, 'mykey.py'), "apikey = 'real-secret'\n"); + await writeFile(join(source, 'frontends', 'desktop_bridge.py'), '# bridge\n'); + + const sandbox = await createSandbox({ + repoRoot: source, + files: ['agentmain.py', 'mykey.py', 'frontends/desktop_bridge.py'], + pythonPath: '/usr/bin/python3', + fakeBaseUrl: 'http://127.0.0.1:23456', + bridgePort: 24168, + vitePort: 25173, + controlToken: 'random-control-token', + }); + + expect(await readFile(join(sandbox.root, 'agentmain.py'), 'utf8')).toContain('current working tree'); + const mykey = await readFile(join(sandbox.root, 'mykey.py'), 'utf8'); + expect(mykey).toContain('http://127.0.0.1:23456/v1'); + expect(mykey).not.toContain('real-secret'); + expect(sandbox.env.HOME).toBe(sandbox.home); + expect(sandbox.env.USERPROFILE).toBe(sandbox.home); + expect(sandbox.env.GA_E2E_SETTINGS_PATH).toBe(join(sandbox.home, '.ga_desktop_settings.json')); + expect(sandbox.env.BRIDGE_PORT).toBe('24168'); + expect(sandbox.env.VITE_BRIDGE_BASE).toBe('http://127.0.0.1:24168'); + expect(sandbox.env.GA_E2E_CONTROL_TOKEN).toBe('random-control-token'); + + await cleanupSandbox(sandbox.root); + }); + + it('refuses non-loopback fake providers before creating a sandbox', async () => { + await expect(createSandbox({ + repoRoot: process.cwd(), + files: [], + pythonPath: '/usr/bin/python3', + fakeBaseUrl: 'https://api.example.com', + bridgePort: 24168, + vitePort: 25173, + controlToken: 'token', + })).rejects.toThrow(/loopback/i); + }); +}); diff --git a/frontends/desktop/e2e/harness/sandbox.ts b/frontends/desktop/e2e/harness/sandbox.ts new file mode 100644 index 000000000..704270dca --- /dev/null +++ b/frontends/desktop/e2e/harness/sandbox.ts @@ -0,0 +1,129 @@ +import { execFileSync } from 'node:child_process'; +import { copyFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { dirname, join, relative, resolve, sep } from 'node:path'; +import { tmpdir } from 'node:os'; +import { assertLoopbackUrl, assertSandboxRoot } from './runtime'; + +export interface SandboxOptions { + repoRoot: string; + pythonPath: string; + fakeBaseUrl: string; + bridgePort: number; + vitePort: number; + controlToken: string; + files?: string[]; +} + +export interface SandboxLayout { + root: string; + home: string; + reports: string; + env: Record; +} + +const EXCLUDED_PREFIXES = [ + '.agents/', '.codex/', '.trellis/', '.claude/', '.git/', + 'frontends/desktop/node_modules/', 'frontends/desktop/dist/', + 'frontends/desktop/src-tauri/target/', 'frontends/desktop/e2e-results/', + 'temp/', '.venv/', +]; + +function workspaceFiles(repoRoot: string): string[] { + const output = execFileSync( + 'git', + ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], + { cwd: repoRoot, encoding: 'utf8' }, + ); + return output.split('\0').filter(Boolean).filter((path) => + !EXCLUDED_PREFIXES.some((prefix) => path === prefix.slice(0, -1) || path.startsWith(prefix)), + ); +} + +function safeRelativePath(path: string): string { + const normalized = path.replaceAll('\\', '/'); + if (!normalized || normalized.startsWith('/') || normalized.split('/').includes('..')) { + throw new Error(`Unsafe workspace path: ${path}`); + } + return normalized; +} + +async function copySelectedFiles(sourceRoot: string, targetRoot: string, files: string[]): Promise { + for (const raw of files) { + const path = safeRelativePath(raw); + const source = resolve(sourceRoot, path); + const target = resolve(targetRoot, path); + const rel = relative(targetRoot, target); + if (rel.startsWith(`..${sep}`) || rel === '..') throw new Error(`Path escaped sandbox: ${path}`); + await mkdir(dirname(target), { recursive: true }); + try { + await copyFile(source, target); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + } +} + +function fakeMykey(fakeBaseUrl: string): string { + const base = `${assertLoopbackUrl(fakeBaseUrl).replace(/\/+$/, '')}/v1`; + return [ + 'native_oai_config = {', + " 'name': 'GenericAgent E2E',", + " 'apikey': 'e2e-dummy-key',", + ` 'apibase': ${JSON.stringify(base)},`, + " 'model': 'e2e-model',", + " 'api_mode': 'chat_completions',", + " 'stream': True,", + " 'max_retries': 0,", + " 'connect_timeout': 2,", + " 'read_timeout': 30,", + '}', + '', + ].join('\n'); +} + +export async function createSandbox(options: SandboxOptions): Promise { + assertLoopbackUrl(options.fakeBaseUrl); + if (!options.controlToken.trim()) throw new Error('E2E control token is required'); + const repoRoot = resolve(options.repoRoot); + const root = await mkdtemp(join(tmpdir(), 'ga-desktop-e2e-')); + const home = join(root, '.home'); + const reports = join(root, 'e2e-report'); + await mkdir(home, { recursive: true }); + await mkdir(reports, { recursive: true }); + await writeFile(join(root, '.ga-e2e-sandbox'), 'v1\n', 'utf8'); + await copySelectedFiles(repoRoot, root, options.files ?? workspaceFiles(repoRoot)); + await writeFile(join(root, 'mykey.py'), fakeMykey(options.fakeBaseUrl), 'utf8'); + await writeFile(join(home, '.ga_desktop_settings.json'), JSON.stringify({ + lang: 'zh', + python_path: options.pythonPath, + project_dir: root, + ui: { llmNo: 0 }, + }, null, 2), 'utf8'); + + const inherited = Object.fromEntries( + Object.entries(process.env).filter((entry): entry is [string, string] => typeof entry[1] === 'string'), + ); + const env = { + ...inherited, + HOME: home, + USERPROFILE: home, + GA_E2E: '1', + GA_E2E_SETTINGS_PATH: join(home, '.ga_desktop_settings.json'), + GA_E2E_CONTROL_TOKEN: options.controlToken, + GA_DESKTOP_E2E_REPORT_DIR: reports, + BRIDGE_HOST: '127.0.0.1', + BRIDGE_PORT: String(options.bridgePort), + VITE_BRIDGE_BASE: `http://127.0.0.1:${options.bridgePort}`, + VITE_GA_E2E: '1', + VITE_PORT: String(options.vitePort), + NO_PROXY: '127.0.0.1,localhost,::1', + no_proxy: '127.0.0.1,localhost,::1', + PYTHONUNBUFFERED: '1', + PYTHONIOENCODING: 'utf-8', + }; + return { root, home, reports, env }; +} + +export async function cleanupSandbox(root: string): Promise { + await rm(assertSandboxRoot(root), { recursive: true, force: true }); +} diff --git a/frontends/desktop/e2e/pages/ChatPage.test.ts b/frontends/desktop/e2e/pages/ChatPage.test.ts new file mode 100644 index 000000000..4cc78efa8 --- /dev/null +++ b/frontends/desktop/e2e/pages/ChatPage.test.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { ChatPage } from './ChatPage'; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('ChatPage native window selection', () => { + it('switches to the main Tauri window before waiting for its renderer', async () => { + const order: string[] = []; + vi.stubGlobal('browser', { + tauri: { + switchWindow: vi.fn(async (label: string) => { + order.push(`switch:${label}`); + }), + }, + }); + + class TestChatPage extends ChatPage { + override async waitUntilReady(): Promise { + order.push('ready'); + } + } + + await new TestChatPage().switchToMainAndWait(); + + expect(order).toEqual(['switch:main', 'ready']); + }); + + it('retries a semantic new-chat lookup while the Tauri renderer is being replaced', async () => { + const click = vi.fn(async () => undefined); + const waitForDisplayed = vi.fn(async () => undefined); + const lookup = vi.fn() + .mockImplementationOnce(() => { + throw new Error('transient WebDriver javascript exception'); + }) + .mockImplementation((selector: string) => { + if (selector.includes('aria-label="New Session"')) { + return { + isDisplayed: vi.fn(async () => true), + isEnabled: vi.fn(async () => true), + click, + }; + } + if (selector.includes('role="textbox"')) return { waitForDisplayed }; + throw new Error(`Unexpected selector: ${selector}`); + }); + vi.stubGlobal('$', lookup); + vi.stubGlobal('browser', { + waitUntil: vi.fn(async (predicate: () => Promise) => { + expect(await predicate()).toBe(false); + expect(await predicate()).toBe(true); + }), + }); + + await new ChatPage().startNewChat(); + + expect(click).toHaveBeenCalledOnce(); + expect(waitForDisplayed).toHaveBeenCalledOnce(); + }); +}); diff --git a/frontends/desktop/e2e/pages/ChatPage.ts b/frontends/desktop/e2e/pages/ChatPage.ts new file mode 100644 index 000000000..0f475892b --- /dev/null +++ b/frontends/desktop/e2e/pages/ChatPage.ts @@ -0,0 +1,70 @@ +export class ChatPage { + get navigation() { return $('nav[aria-label="Main navigation"]'); } + get editor() { return $('[role="textbox"][contenteditable="true"]'); } + get sendButton() { return $('button[aria-label="Send message"]'); } + get assistants() { return $$('[data-role="assistant"]'); } + + async waitUntilReady(): Promise { + await this.navigation.waitForDisplayed({ timeout: 20_000 }); + } + + async switchToMainAndWait(): Promise { + // Windows WebDriver can initially attach to the visible setup window even + // after bootstrap has navigated and shown the main window. + await browser.tauri.switchWindow('main'); + await this.waitUntilReady(); + } + + async waitForBridgeReady(): Promise { + await browser.waitUntil(async () => !(await $('.ga-chat-offline').isExisting()), { + timeout: 30_000, + interval: 200, + timeoutMsg: 'Bridge did not reach ready state in the UI', + }); + } + + async startNewChat(): Promise { + let button: ReturnType | undefined; + await browser.waitUntil(async () => { + try { + const candidate = $('button[aria-label="新建会话"], button[aria-label="New Session"]'); + if (!await candidate.isDisplayed() || !await candidate.isEnabled()) return false; + button = candidate; + return true; + } catch { + // Tauri may replace the renderer while bootstrap reopens the main UI. + return false; + } + }, { + timeout: 20_000, + interval: 100, + timeoutMsg: 'New chat action did not become available', + }); + await button!.click(); + await this.editor.waitForDisplayed({ timeout: 10_000 }); + } + + async send(text: string): Promise { + const editor = await this.editor; + await editor.click(); + await browser.execute((element, value) => { + const target = element as unknown as HTMLElement; + target.textContent = value; + target.dispatchEvent(new InputEvent('input', { + bubbles: true, + inputType: 'insertText', + data: value, + })); + }, editor, text); + await this.sendButton.waitForEnabled({ timeout: 5_000 }); + await this.sendButton.click(); + } + + async waitForAssistantText(text: string, timeout = 30_000): Promise { + await browser.waitUntil(async () => { + const items = await this.assistants as unknown as WebdriverIO.Element[]; + if (!items.length) return false; + return (await items[items.length - 1].getText()).includes(text); + }, { timeout, interval: 200, timeoutMsg: `Assistant did not render: ${text}` }); + } +} diff --git a/frontends/desktop/e2e/pages/RecoveryPage.ts b/frontends/desktop/e2e/pages/RecoveryPage.ts new file mode 100644 index 000000000..c3c78a1a3 --- /dev/null +++ b/frontends/desktop/e2e/pages/RecoveryPage.ts @@ -0,0 +1,35 @@ +export class RecoveryPage { + get offlineBanner() { return $('.ga-chat-offline'); } + get restartButton() { return $('[data-testid="bridge-restart"]'); } + + async waitForOffline(timeout = 20_000): Promise { + await this.offlineBanner.waitForDisplayed({ timeout }); + } + + async recoverFromServices(): Promise { + const services = await $('nav[aria-label="Main navigation"] button[aria-label="后台服务"], nav[aria-label="Main navigation"] button[aria-label="Services"]'); + await services.click(); + await this.restartButton.waitForDisplayed({ timeout: 20_000 }); + await this.restartButton.click(); + } + + async retryFromServices(): Promise { + await browser.waitUntil(async () => { + try { + const buttons = await $$('[data-testid="bridge-restart"]') as unknown as WebdriverIO.Element[]; + for (const button of buttons) { + if (await button.isDisplayed() && await button.isEnabled()) return true; + } + } catch { /* the status poll may replace the button between queries */ } + return false; + }, { timeout: 20_000, interval: 100, timeoutMsg: 'Restart action did not become available' }); + const buttons = await $$('[data-testid="bridge-restart"]') as unknown as WebdriverIO.Element[]; + for (const button of buttons) { + if (await button.isDisplayed() && await button.isEnabled()) { + await button.click(); + return; + } + } + throw new Error('Visible restart action disappeared before it could be clicked'); + } +} diff --git a/frontends/desktop/e2e/pages/UsagePage.ts b/frontends/desktop/e2e/pages/UsagePage.ts new file mode 100644 index 000000000..50ba38970 --- /dev/null +++ b/frontends/desktop/e2e/pages/UsagePage.ts @@ -0,0 +1,19 @@ +export class UsagePage { + get totalTokens() { return $('[data-testid="token-stat-value-tok.total"]'); } + get cacheRate() { return $('[data-testid="token-stat-value-tok.cost"]'); } + + async open(): Promise { + const button = await $('nav[aria-label="Main navigation"] button[aria-label="用量"], nav[aria-label="Main navigation"] button[aria-label="Usage"]'); + await button.waitForExist({ timeout: 10_000 }); + await button.click(); + await this.totalTokens.waitForDisplayed({ timeout: 10_000 }); + } + + async waitForTotal(expected: string, timeout = 20_000): Promise { + await browser.waitUntil(async () => (await this.totalTokens.getText()) === expected, { + timeout, + interval: 250, + timeoutMsg: `Expected total token count ${expected}`, + }); + } +} diff --git a/frontends/desktop/e2e/requirements.txt b/frontends/desktop/e2e/requirements.txt new file mode 100644 index 000000000..62a15c16f --- /dev/null +++ b/frontends/desktop/e2e/requirements.txt @@ -0,0 +1,8 @@ +pytest +aiohttp>=3.9 +requests>=2.28 +psutil +fastapi +uvicorn +websockets +pydantic diff --git a/frontends/desktop/e2e/run.ts b/frontends/desktop/e2e/run.ts new file mode 100644 index 000000000..5f810ad47 --- /dev/null +++ b/frontends/desktop/e2e/run.ts @@ -0,0 +1,104 @@ +import { existsSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { spawn, spawnSync } from 'node:child_process'; +import { DesktopE2EHarness } from './harness/orchestrator'; + +function argument(name: string): string | undefined { + const prefix = `--${name}=`; + return process.argv.find((value) => value.startsWith(prefix))?.slice(prefix.length); +} + +function pythonPath(repoRoot: string): string { + const configured = process.env.GA_E2E_PYTHON; + if (configured) return configured; + const worktrees = spawnSync('git', ['worktree', 'list', '--porcelain'], { + cwd: repoRoot, + encoding: 'utf8', + }).stdout.split(/\r?\n/).filter((line) => line.startsWith('worktree ')).map((line) => line.slice(9)); + const roots = [repoRoot, ...worktrees]; + const candidates = process.platform === 'win32' + ? [...roots.map((root) => resolve(root, '.venv', 'Scripts', 'python.exe')), 'python'] + : [ + ...(process.env.VIRTUAL_ENV ? [resolve(process.env.VIRTUAL_ENV, 'bin', 'python')] : []), + ...roots.map((root) => resolve(root, '.venv', 'bin', 'python')), + 'python3', + 'python', + ]; + const compatible = candidates.find((candidate) => { + if (candidate.includes('/') && !existsSync(candidate)) return false; + return spawnSync(candidate, ['-c', 'import aiohttp'], { stdio: 'ignore' }).status === 0; + }); + if (!compatible) throw new Error('No Python runtime with aiohttp found; set GA_E2E_PYTHON'); + return compatible; +} + +async function run(): Promise { + const mode = argument('mode') === 'desktop' ? 'desktop' : 'browser'; + const suite = argument('suite') === 'full' ? 'full' : 'smoke'; + const desktopRoot = resolve(process.cwd()); + const repoRoot = resolve(desktopRoot, '..', '..'); + const harness = new DesktopE2EHarness({ + mode, + desktopRoot, + pythonPath: pythonPath(repoRoot), + application: process.env.GA_E2E_APPLICATION, + }); + let exitCode = 1; + try { + const context = await harness.start(); + if (mode === 'desktop' && !context.application) { + const build = spawn('npm', [ + 'run', 'tauri', 'build', '--', '--no-bundle', '--features', 'e2e', + '--config', 'src-tauri/tauri.e2e.conf.json', + ], { + cwd: desktopRoot, + env: { + ...process.env, + ...context.appEnv, + HOME: process.env.HOME, + USERPROFILE: process.env.USERPROFILE, + }, + stdio: 'inherit', + shell: process.platform === 'win32', + }); + const buildCode = await new Promise((resolveExit, reject) => { + build.once('error', reject); + build.once('exit', (code) => resolveExit(code ?? 1)); + }); + if (buildCode !== 0) throw new Error(`Tauri E2E build failed with exit code ${buildCode}`); + const binary = resolve( + desktopRoot, + 'src-tauri', + 'target', + 'release', + process.platform === 'win32' ? 'ga-desktop.exe' : 'ga-desktop', + ); + if (!existsSync(binary)) throw new Error(`Tauri E2E binary not found: ${binary}`); + context.application = binary; + writeFileSync(harness.contextFile, JSON.stringify(context, null, 2), 'utf8'); + } + const config = resolve(desktopRoot, 'e2e', `wdio.${mode}.conf.ts`); + const wdio = resolve(desktopRoot, 'node_modules', '@wdio', 'cli', 'bin', 'wdio.js'); + const child = spawn(process.execPath, [wdio, 'run', config], { + cwd: desktopRoot, + env: { ...process.env, GA_E2E_CONTEXT_FILE: harness.contextFile, GA_E2E_SUITE: suite }, + stdio: 'inherit', + }); + exitCode = await new Promise((resolveExit, reject) => { + child.once('error', reject); + child.once('exit', (code) => resolveExit(code ?? 1)); + }); + if (exitCode !== 0) harness.markFailed(); + } catch (error) { + harness.markFailed(); + throw error; + } finally { + await harness.stop(); + } + process.exitCode = exitCode; +} + +void run().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.stack : error}\n`); + process.exitCode = 1; +}); diff --git a/frontends/desktop/e2e/specs/browser/critical-loops.e2e.ts b/frontends/desktop/e2e/specs/browser/critical-loops.e2e.ts new file mode 100644 index 000000000..2c2c3d66b --- /dev/null +++ b/frontends/desktop/e2e/specs/browser/critical-loops.e2e.ts @@ -0,0 +1,102 @@ +import assert from 'node:assert/strict'; +import { ChatPage } from '../../pages/ChatPage'; +import { UsagePage } from '../../pages/UsagePage'; +import { controlRequest, loadE2EContext } from '../../harness/context'; + +const chat = new ChatPage(); +const usage = new UsagePage(); +const context = loadE2EContext(); + +interface TokenHistory { + history: Array<{ input: number; output: number; cacheRead: number }>; + snap: Record; +} + +async function history(): Promise { + const response = await fetch(`${context.bridgeBase}/token-history`); + assert.equal(response.status, 200); + return await response.json() as TokenHistory; +} + +function totals(value: TokenHistory) { + return Object.values(value.snap).reduce((sum, item) => ({ + input: sum.input + item.input, + output: sum.output + item.output, + cacheRead: sum.cacheRead + item.cacheRead, + }), { input: 0, output: 0, cacheRead: 0 }); +} + +async function waitForTotals(expected: { input: number; output: number; cacheRead: number }): Promise { + await browser.waitUntil(async () => { + const actual = totals(await history()); + return actual.input === expected.input && actual.output === expected.output && actual.cacheRead === expected.cacheRead; + }, { timeout: 30_000, interval: 250, timeoutMsg: `Token history did not reach ${JSON.stringify(expected)}` }); +} + +async function armEmptyTurn(): Promise { + const response = await fetch(`${context.bridgeBase}/__e2e__/next-turn`, { + method: 'POST', + headers: { 'content-type': 'application/json', 'x-ga-e2e-token': context.controlToken }, + body: JSON.stringify({ mode: 'empty' }), + }); + assert.equal(response.status, 200); +} + +describe('GenericAgent critical desktop loops in browser mode', () => { + before(async () => { + await browser.url('/'); + await chat.waitUntilReady(); + }); + + it('migrates legacy JSON history exactly once across bridge restarts', async () => { + assert.deepEqual(totals(await history()), { input: 2, output: 3, cacheRead: 1 }); + await controlRequest('/bridge/restart', { method: 'POST', body: '{}' }); + assert.deepEqual(totals(await history()), { input: 2, output: 3, cacheRead: 1 }); + }); + + it('persists exact per-call usage and survives corrupt JSONL plus restart', async () => { + await chat.startNewChat(); + await chat.send('[E2E:normal] deterministic ledger'); + await chat.waitForAssistantText('Harness reply'); + await waitForTotals({ input: 92, output: 20, cacheRead: 12 }); + + await usage.open(); + await usage.waitForTotal('112'); + assert.equal(await usage.cacheRate.getText(), '11.5%'); + + await controlRequest('/ledger/corrupt-tail', { method: 'POST', body: '{}' }); + assert.deepEqual(totals(await history()), { input: 92, output: 20, cacheRead: 12 }); + await controlRequest('/bridge/restart', { method: 'POST', body: '{}' }); + assert.deepEqual(totals(await history()), { input: 92, output: 20, cacheRead: 12 }); + await browser.refresh(); + await chat.waitUntilReady(); + await usage.open(); + await usage.waitForTotal('112'); + }); + + it('renders Chinese and English empty-turn fallbacks as assistant prose', async () => { + await chat.startNewChat(); + await armEmptyTurn(); + await chat.send('empty zh'); + await chat.waitForAssistantText('这一轮结束了,但没有产出可见回复'); + + await controlRequest('/settings/language', { method: 'POST', body: JSON.stringify({ lang: 'en' }) }); + await browser.refresh(); + await chat.waitUntilReady(); + await chat.startNewChat(); + await armEmptyTurn(); + await chat.send('empty en'); + await chat.waitForAssistantText('This turn ended without a visible response'); + }); + + it('keeps the completed first API call after a hard bridge crash during call two', async () => { + await chat.startNewChat(); + await chat.send('[E2E:two-call-hang] crash recovery'); + await controlRequest('/bridge/crash-after-second-call', { method: 'POST', body: '{}' }); + await waitForTotals({ input: 182, output: 37, cacheRead: 23 }); + await browser.refresh(); + await chat.waitUntilReady(); + await usage.open(); + await usage.waitForTotal('219'); + }); +}); diff --git a/frontends/desktop/e2e/specs/desktop/full.e2e.ts b/frontends/desktop/e2e/specs/desktop/full.e2e.ts new file mode 100644 index 000000000..af1bc161d --- /dev/null +++ b/frontends/desktop/e2e/specs/desktop/full.e2e.ts @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict'; +import { ChatPage } from '../../pages/ChatPage'; +import { RecoveryPage } from '../../pages/RecoveryPage'; +import { controlRequest, loadE2EContext } from '../../harness/context'; +import { pathsReferToSameEntry } from '../../harness/runtime'; + +const chat = new ChatPage(); +const recovery = new RecoveryPage(); +const context = loadE2EContext(); +const describeFull = process.env.GA_E2E_SUITE === 'full' ? describe : describe.skip; + +describeFull('GenericAgent native Tauri full recovery', () => { + it('rejects a foreign bridge port owner and recovers after retry', async () => { + await chat.switchToMainAndWait(); + await chat.waitForBridgeReady(); + + await controlRequest('/port/occupy-bridge', { method: 'POST', body: '{}' }); + await chat.startNewChat(); + await recovery.waitForOffline(); + await recovery.recoverFromServices(); + const blockedIdentity = await (await fetch(`${context.bridgeBase}/services/identity`)).json() as { + service?: string; + ga_root?: string; + }; + assert.equal(blockedIdentity.service, 'foreign-e2e-listener'); + assert.equal(blockedIdentity.ga_root, undefined); + + await controlRequest('/port/release-bridge', { method: 'POST', body: '{}' }); + await recovery.retryFromServices(); + await browser.waitUntil(async () => { + try { + const response = await fetch(`${context.bridgeBase}/services/identity`); + if (!response.ok) return false; + const identity = await response.json() as { ga_root?: string }; + return Boolean(identity.ga_root) + && pathsReferToSameEntry(identity.ga_root!, context.sandboxRoot); + } catch { + return false; + } + }, { timeout: 30_000, interval: 250, timeoutMsg: 'Bridge did not recover after releasing the foreign port owner' }); + await chat.startNewChat(); + await chat.waitForBridgeReady(); + }); +}); diff --git a/frontends/desktop/e2e/specs/desktop/smoke.e2e.ts b/frontends/desktop/e2e/specs/desktop/smoke.e2e.ts new file mode 100644 index 000000000..f7db743ef --- /dev/null +++ b/frontends/desktop/e2e/specs/desktop/smoke.e2e.ts @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict'; +import { ChatPage } from '../../pages/ChatPage'; +import { UsagePage } from '../../pages/UsagePage'; +import { RecoveryPage } from '../../pages/RecoveryPage'; +import { controlRequest, loadE2EContext } from '../../harness/context'; +import { pathsReferToSameEntry } from '../../harness/runtime'; + +const chat = new ChatPage(); +const usage = new UsagePage(); +const recovery = new RecoveryPage(); +const context = loadE2EContext(); + +describe('GenericAgent native Tauri smoke', () => { + it('boots in the isolated sandbox and completes chat plus usage UI', async () => { + await chat.switchToMainAndWait(); + const identity = await (await fetch(`${context.bridgeBase}/services/identity`)).json() as { ga_root: string }; + assert.ok(pathsReferToSameEntry(identity.ga_root, context.sandboxRoot), 'bridge must run inside the E2E sandbox'); + await chat.waitForBridgeReady(); + await chat.startNewChat(); + await chat.send('[E2E:normal] native smoke'); + await chat.waitForAssistantText('Harness reply', 60_000); + await usage.open(); + await usage.waitForTotal('107', 30_000); + + await controlRequest('/bridge/kill-external', { method: 'POST', body: '{}' }); + await chat.startNewChat(); + await recovery.waitForOffline(); + await recovery.recoverFromServices(); + await browser.waitUntil(async () => { + try { + return (await fetch(`${context.bridgeBase}/services/identity`)).ok; + } catch { + return false; + } + }, { timeout: 30_000, interval: 250, timeoutMsg: 'Bridge did not restart through the native UI' }); + await chat.startNewChat(); + await chat.waitForBridgeReady(); + }); +}); diff --git a/frontends/desktop/e2e/wdio.browser.conf.ts b/frontends/desktop/e2e/wdio.browser.conf.ts new file mode 100644 index 000000000..fa6a8eeb7 --- /dev/null +++ b/frontends/desktop/e2e/wdio.browser.conf.ts @@ -0,0 +1,40 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { loadE2EContext } from './harness/context'; + +const context = loadE2EContext(); +if (!context.viteUrl) throw new Error('Browser E2E context is missing viteUrl'); +const e2eRoot = dirname(fileURLToPath(import.meta.url)); + +export const config: WebdriverIO.Config = { + runner: 'local', + specs: [join(e2eRoot, 'specs', 'browser', '**', '*.e2e.ts')], + maxInstances: 1, + logLevel: 'error', + bail: 0, + baseUrl: context.viteUrl, + waitforTimeout: 10_000, + connectionRetryTimeout: 60_000, + connectionRetryCount: 0, + services: [[ + '@wdio/tauri-service', + { mode: 'browser', devServerUrl: context.viteUrl, logLevel: 'error' }, + ]], + capabilities: [{ + browserName: 'tauri', + 'goog:chromeOptions': { + args: ['--headless=new', '--disable-gpu', '--no-sandbox', '--window-size=1440,1000'], + }, + } as WebdriverIO.Capabilities], + framework: 'mocha', + reporters: ['spec'], + mochaOpts: { ui: 'bdd', timeout: 60_000 }, + afterTest: async function (test, _context, result) { + if (result.passed) return; + mkdirSync(context.reports, { recursive: true }); + const safeName = test.title.replace(/[^a-z0-9_-]+/gi, '-').slice(0, 80); + await browser.saveScreenshot(join(context.reports, `${safeName}.png`)); + writeFileSync(join(context.reports, `${safeName}.html`), await browser.getPageSource(), 'utf8'); + }, +}; diff --git a/frontends/desktop/e2e/wdio.desktop.conf.ts b/frontends/desktop/e2e/wdio.desktop.conf.ts new file mode 100644 index 000000000..a5a1d5865 --- /dev/null +++ b/frontends/desktop/e2e/wdio.desktop.conf.ts @@ -0,0 +1,48 @@ +import { mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { loadE2EContext } from './harness/context'; + +const context = loadE2EContext(); +if (!context.application) throw new Error('Desktop E2E context is missing application path'); +const e2eRoot = dirname(fileURLToPath(import.meta.url)); +const smokeSpec = join(e2eRoot, 'specs', 'desktop', 'smoke.e2e.ts'); +const fullSpec = join(e2eRoot, 'specs', 'desktop', 'full.e2e.ts'); + +export const config: WebdriverIO.Config = { + runner: 'local', + specs: process.env.GA_E2E_SUITE === 'full' ? [smokeSpec, fullSpec] : [smokeSpec], + maxInstances: 1, + logLevel: 'error', + bail: 0, + waitforTimeout: 15_000, + connectionRetryTimeout: 90_000, + connectionRetryCount: 0, + services: [[ + '@wdio/tauri-service', + { + appBinaryPath: context.application, + driverProvider: 'embedded', + logLevel: 'error', + env: context.appEnv, + captureBackendLogs: true, + captureFrontendLogs: true, + logDir: context.reports, + startTimeout: 60_000, + }, + ]], + capabilities: [{ + browserName: 'tauri', + 'tauri:options': { application: context.application }, + } as WebdriverIO.Capabilities], + framework: 'mocha', + reporters: ['spec'], + mochaOpts: { ui: 'bdd', timeout: 90_000 }, + afterTest: async function (test, _context, result) { + if (result.passed) return; + mkdirSync(context.reports, { recursive: true }); + const safeName = test.title.replace(/[^a-z0-9_-]+/gi, '-').slice(0, 80); + await browser.saveScreenshot(join(context.reports, `${safeName}.png`)); + writeFileSync(join(context.reports, `${safeName}.html`), await browser.getPageSource(), 'utf8'); + }, +}; diff --git a/frontends/desktop/e2e/windows/Invoke-WindowsUserJourney.ps1 b/frontends/desktop/e2e/windows/Invoke-WindowsUserJourney.ps1 new file mode 100644 index 000000000..2d88e18a0 --- /dev/null +++ b/frontends/desktop/e2e/windows/Invoke-WindowsUserJourney.ps1 @@ -0,0 +1,567 @@ +[CmdletBinding()] +param( + [string]$Repo = "abraxas914/GenericAgent", + [Parameter(Mandatory = $true)] + [string]$RunId, + [string]$ExpectedCommit = "", + [string]$PackageZip = "", + [string]$WorkDir = "", + [ValidateSet("Full", "Smoke", "FailureOnly")] + [string]$Mode = "Full", + [switch]$KeepWorkDir +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 + +if (-not $WorkDir) { + $WorkDir = Join-Path $env:TEMP ("ga-desktop-e2e\run-" + $RunId) +} + +$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$WorkDir = [System.IO.Path]::GetFullPath($WorkDir) +$DownloadDir = Join-Path $WorkDir "download" +$ExtractDir = Join-Path $WorkDir "app" +$ReportDir = Join-Path $WorkDir "report" +$ScreensDir = Join-Path $ReportDir "screenshots" +$ReportPath = Join-Path $ReportDir "e2e-report.json" +$RunSucceeded = $false + +$Report = [ordered]@{ + repo = $Repo + runId = $RunId + expectedCommit = $ExpectedCommit + mode = $Mode + package = [ordered]@{} + environment = [ordered]@{} + checks = [ordered]@{} + screenshots = @() + manualChecklist = [ordered]@{ + windowsTitlebarVisible = "manual" + exactlyThreeWindowButtons = "manual" + titlebarBlankAreaDrags = "manual" + buttonAreaDoesNotDrag = "manual" + minimizeWorks = "manual" + maximizeRestoreWorks = "manual" + closeHidesToTray = "manual" + sidebarNavHasNoBlankRow = "manual" + } + failures = @() + startedAt = (Get-Date).ToUniversalTime().ToString("o") +} + +function Write-Step([string]$Message) { + Write-Host "" + Write-Host "==> $Message" +} + +function Add-Failure([string]$Message) { + $script:Report.failures += $Message +} + +function Save-Report { + $script:Report.completedAt = (Get-Date).ToUniversalTime().ToString("o") + $json = $script:Report | ConvertTo-Json -Depth 12 + [System.IO.Directory]::CreateDirectory($ReportDir) | Out-Null + [System.IO.File]::WriteAllText($ReportPath, $json, [System.Text.UTF8Encoding]::new($false)) +} + +function Fail([string]$Message) { + Add-Failure $Message + Save-Report + throw $Message +} + +function Ensure-Dir([string]$Path) { + [System.IO.Directory]::CreateDirectory($Path) | Out-Null +} + +function Test-Command([string]$Name) { + $cmd = Get-Command $Name -ErrorAction SilentlyContinue + return $null -ne $cmd +} + +function Normalize-Commit([string]$Commit) { + return ($Commit -replace "\s", "").ToLowerInvariant() +} + +function Test-CommitMatches([string]$Actual, [string]$Expected) { + if (-not $Expected) { return $true } + $actualNorm = Normalize-Commit $Actual + $expectedNorm = Normalize-Commit $Expected + return $actualNorm.StartsWith($expectedNorm) -or $expectedNorm.StartsWith($actualNorm) +} + +function Get-RunMetadata { + if (-not (Test-Command "gh")) { + Fail "GitHub CLI 'gh' is required when -PackageZip is not enough to skip run metadata." + } + $json = & gh run view $RunId --repo $Repo --json headSha,status,conclusion,event,name 2>&1 + if ($LASTEXITCODE -ne 0) { + Fail "gh run view failed: $json" + } + $meta = ($json | Out-String) | ConvertFrom-Json + $script:Report.run = $meta + if ($ExpectedCommit -and -not (Test-CommitMatches $meta.headSha $ExpectedCommit)) { + Fail "Run headSha $($meta.headSha) does not match expected commit $ExpectedCommit" + } + return $meta +} + +function Resolve-PackageZip { + Ensure-Dir $DownloadDir + if ($PackageZip) { + $zipPath = [System.IO.Path]::GetFullPath($PackageZip) + if (-not (Test-Path -LiteralPath $zipPath)) { + Fail "Package zip not found: $zipPath" + } + return $zipPath + } + + if (-not (Test-Command "gh")) { + Fail "GitHub CLI 'gh' is required to download artifacts." + } + + Write-Step "Download GitHub Actions artifact" + $downloadOutput = & gh run download $RunId --repo $Repo --dir $DownloadDir 2>&1 + if ($LASTEXITCODE -ne 0) { + Fail "gh run download failed: $downloadOutput" + } + + $zips = @(Get-ChildItem -LiteralPath $DownloadDir -Recurse -Filter "GenericAgent-Desktop-Windows-Portable.zip") + if ($zips.Count -ne 1) { + Fail "Expected exactly one GenericAgent-Desktop-Windows-Portable.zip under $DownloadDir, found $($zips.Count)" + } + return $zips[0].FullName +} + +function Test-Sha256([string]$ZipPath) { + Write-Step "Verify SHA-256" + $hash = (Get-FileHash -LiteralPath $ZipPath -Algorithm SHA256).Hash.ToLowerInvariant() + $shaFile = $ZipPath + ".sha256" + if (-not (Test-Path -LiteralPath $shaFile)) { + $matches = @(Get-ChildItem -LiteralPath (Split-Path -Parent $ZipPath) -Recurse -Filter "*.sha256") + if ($matches.Count -eq 1) { + $shaFile = $matches[0].FullName + } + } + if (Test-Path -LiteralPath $shaFile) { + $expectedText = [System.IO.File]::ReadAllText($shaFile) + $expectedHash = ([regex]::Match($expectedText, "[A-Fa-f0-9]{64}")).Value.ToLowerInvariant() + if (-not $expectedHash) { + Fail "Could not parse SHA-256 from $shaFile" + } + if ($hash -ne $expectedHash) { + Fail "SHA-256 mismatch. actual=$hash expected=$expectedHash" + } + $script:Report.package.sha256File = $shaFile + $script:Report.package.expectedSha256 = $expectedHash + } else { + $script:Report.package.expectedSha256 = $null + $script:Report.checks.sha256Sidecar = "missing" + } + $script:Report.package.sha256 = $hash +} + +function Expand-Package([string]$ZipPath) { + Write-Step "Extract package" + if (Test-Path -LiteralPath $ExtractDir) { + Remove-Item -LiteralPath $ExtractDir -Recurse -Force + } + Ensure-Dir $ExtractDir + Expand-Archive -LiteralPath $ZipPath -DestinationPath $ExtractDir -Force + + $packageRoot = Join-Path $ExtractDir "GenericAgent-Desktop-Windows-Portable" + if (-not (Test-Path -LiteralPath $packageRoot)) { + $dirs = @(Get-ChildItem -LiteralPath $ExtractDir -Directory) + if ($dirs.Count -eq 1) { + $packageRoot = $dirs[0].FullName + } + } + if (-not (Test-Path -LiteralPath $packageRoot)) { + Fail "Could not find extracted package root under $ExtractDir" + } + return [System.IO.Path]::GetFullPath($packageRoot) +} + +function Assert-PackageShape([string]$PackageRoot) { + Write-Step "Validate package structure" + $required = @( + "GenericAgent.exe", + "runtime\install_windows.ps1", + "runtime\wheels", + "runtime\python", + "runtime\app\frontends\desktop_bridge.py" + ) + foreach ($rel in $required) { + $path = Join-Path $PackageRoot $rel + if (-not (Test-Path -LiteralPath $path)) { + Fail "Package is missing required path: $rel" + } + } + $script:Report.package.root = $PackageRoot + $script:Report.package.exe = Join-Path $PackageRoot "GenericAgent.exe" + $script:Report.package.form = "windows-portable-zip" +} + +function Get-WindowsEnvironment { + $os = Get-CimInstance Win32_OperatingSystem + $cpu = Get-CimInstance Win32_Processor | Select-Object -First 1 + $arch = if ($env:PROCESSOR_ARCHITECTURE) { $env:PROCESSOR_ARCHITECTURE } else { [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() } + return [ordered]@{ + caption = $os.Caption + version = $os.Version + buildNumber = $os.BuildNumber + architecture = $arch + cpu = $cpu.Name + userInteractive = [Environment]::UserInteractive + webView2Installed = Test-WebView2Installed + } +} + +function Test-WebView2Installed { + $keys = @( + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}", + "HKLM:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}", + "HKCU:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" + ) + foreach ($key in $keys) { + if (Test-Path $key) { return $true } + } + return $false +} + +function Get-PortState { + try { + $connections = @(Get-NetTCPConnection -LocalAddress "127.0.0.1" -LocalPort 14168 -ErrorAction SilentlyContinue) + return @($connections | Select-Object LocalAddress, LocalPort, State, OwningProcess) + } catch { + return @() + } +} + +function Get-GaProcesses { + return @(Get-Process | Where-Object { + $_.ProcessName -like "GenericAgent*" -or $_.ProcessName -like "ga-desktop*" -or $_.ProcessName -like "python*" + } | Select-Object ProcessName, Id, Path) +} + +function Stop-ProcessTreeSafe([int]$ProcessId) { + try { + Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue + } catch {} +} + +function Capture-Screenshot([string]$Name) { + Ensure-Dir $ScreensDir + $path = Join-Path $ScreensDir $Name + try { + Add-Type -AssemblyName System.Windows.Forms + Add-Type -AssemblyName System.Drawing + $bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds + $bitmap = [System.Drawing.Bitmap]::new($bounds.Width, $bounds.Height) + $graphics = [System.Drawing.Graphics]::FromImage($bitmap) + $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size) + $bitmap.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) + $graphics.Dispose() + $bitmap.Dispose() + $script:Report.screenshots += $path + return $path + } catch { + Add-Failure "Screenshot failed for ${Name}: $($_.Exception.Message)" + return $null + } +} + +function Wait-ForBootstrapPhase( + [string]$ReportDirectory, + [string]$Phase, + [int]$TimeoutSeconds, + [int]$AfterSequence = 0 +) { + $latest = Join-Path $ReportDirectory "bootstrap-latest.json" + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if (Test-Path -LiteralPath $latest) { + try { + $snapshot = [System.IO.File]::ReadAllText($latest) | ConvertFrom-Json + if ([int]$snapshot.seq -le $AfterSequence) { + Start-Sleep -Milliseconds 500 + continue + } + if ($snapshot.phase -eq $Phase) { + return $snapshot + } + if ($Phase -ne "failed" -and $snapshot.phase -eq "failed") { + return $snapshot + } + } catch {} + } + Start-Sleep -Milliseconds 500 + } + return $null +} + +function Wait-ForBootstrapAnyPhase([string]$ReportDirectory, [string[]]$Phases, [int]$TimeoutSeconds) { + $latest = Join-Path $ReportDirectory "bootstrap-latest.json" + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if (Test-Path -LiteralPath $latest) { + try { + $snapshot = [System.IO.File]::ReadAllText($latest) | ConvertFrom-Json + if ($Phases -contains [string]$snapshot.phase) { + return $snapshot + } + } catch {} + } + Start-Sleep -Milliseconds 500 + } + return $null +} + +function Wait-ForBridgeIdentity([string]$ExpectedRoot, [int]$TimeoutSeconds) { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + try { + $identity = Invoke-RestMethod -Uri "http://127.0.0.1:14168/services/identity" -TimeoutSec 2 + if ($identity.ga_root) { + $reported = [System.IO.Path]::GetFullPath([string]$identity.ga_root).TrimEnd('\') + $expected = [System.IO.Path]::GetFullPath($ExpectedRoot).TrimEnd('\') + if ($reported.Equals($expected, [System.StringComparison]::OrdinalIgnoreCase)) { + return $identity + } + } + } catch {} + Start-Sleep -Milliseconds 700 + } + return $null +} + +function Start-GenericAgent([string]$PackageRoot, [string]$Scenario) { + $exe = Join-Path $PackageRoot "GenericAgent.exe" + $scenarioReportDir = Join-Path $ReportDir $Scenario + Ensure-Dir $scenarioReportDir + $env:GA_DESKTOP_E2E_REPORT_DIR = $scenarioReportDir + try { + return Start-Process -FilePath $exe -WorkingDirectory $PackageRoot -PassThru + } finally { + Remove-Item Env:\GA_DESKTOP_E2E_REPORT_DIR -ErrorAction SilentlyContinue + } +} + +function Stop-GenericAgent { + foreach ($proc in @(Get-Process GenericAgent -ErrorAction SilentlyContinue)) { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + } + foreach ($proc in @(Get-Process ga-desktop -ErrorAction SilentlyContinue)) { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + } +} + +function Stop-BridgeOnPort { + foreach ($conn in @(Get-PortState)) { + if ($conn.OwningProcess) { + try { + $proc = Get-Process -Id $conn.OwningProcess -ErrorAction Stop + if ($proc.ProcessName -like "python*" -or $proc.ProcessName -like "GenericAgent*" -or $proc.ProcessName -like "ga-desktop*") { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + } + } catch {} + } + } +} + +function Invoke-FreshLaunch([string]$PackageRoot) { + Write-Step "Fresh launch and first-run prepare" + $marker = Join-Path $PackageRoot "runtime\.prepared" + if (Test-Path -LiteralPath $marker) { + Fail "Fresh package already has runtime\\.prepared before launch" + } + + $proc = Start-GenericAgent $PackageRoot "fresh" + $script:Report.checks.freshProcessId = $proc.Id + Start-Sleep -Seconds 2 + Capture-Screenshot "loading-first.png" | Out-Null + $prepSnapshot = Wait-ForBootstrapAnyPhase (Join-Path $ReportDir "fresh") @("preparing", "ready", "failed") 30 + if ($prepSnapshot -and $prepSnapshot.phase -eq "preparing") { + Capture-Screenshot "preparing.png" | Out-Null + } else { + $prepPhase = if ($prepSnapshot) { [string]$prepSnapshot.phase } else { "unavailable" } + $script:Report.checks.preparingScreenshot = "not captured; phase was $prepPhase" + } + + $snapshot = Wait-ForBootstrapPhase (Join-Path $ReportDir "fresh") "ready" 240 + if (-not $snapshot -or $snapshot.phase -ne "ready") { + Capture-Screenshot "fresh-failed.png" | Out-Null + Fail "Fresh launch did not reach bootstrap ready" + } + + if (-not (Test-Path -LiteralPath $marker)) { + Fail "Fresh launch reached ready but runtime\\.prepared was not created" + } + $identity = Wait-ForBridgeIdentity (Join-Path $PackageRoot "runtime\app") 30 + if (-not $identity) { + Fail "Bridge identity did not match extracted runtime app" + } + Capture-Screenshot "main-ready.png" | Out-Null + $script:Report.checks.freshReady = $true + $script:Report.checks.bridgeIdentity = $identity +} + +function Start-ForeignPortListener { + Write-Step "Start unknown foreign listener on 127.0.0.1:14168" + $ready = Join-Path $WorkDir "foreign-listener.ready" + Remove-Item -LiteralPath $ready -Force -ErrorAction SilentlyContinue + $fixture = Join-Path $ScriptRoot "fixtures\foreign-port-listener.ps1" + $proc = Start-Process -FilePath "powershell.exe" -ArgumentList @( + "-NoProfile", + "-ExecutionPolicy", "Bypass", + "-File", $fixture, + "-Port", "14168", + "-ReadyFile", $ready + ) -WindowStyle Hidden -PassThru + $deadline = (Get-Date).AddSeconds(20) + while ((Get-Date) -lt $deadline) { + if ((Test-Path -LiteralPath $ready) -and (Get-PortState).Count -gt 0) { + return $proc + } + Start-Sleep -Milliseconds 250 + } + Stop-ProcessTreeSafe $proc.Id + Fail "Foreign listener did not bind 127.0.0.1:14168" +} + +function Invoke-SetupRetry( + [System.Diagnostics.Process]$AppProcess, + [int]$TimeoutSeconds, + [int]$AfterSequence +) { + Add-Type -AssemblyName System.Windows.Forms + $sig = @" +using System; +using System.Runtime.InteropServices; +public static class Win32Focus { + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); +} +"@ + Add-Type -TypeDefinition $sig -ErrorAction SilentlyContinue + try { + $AppProcess.Refresh() + if ($AppProcess.MainWindowHandle -ne [IntPtr]::Zero) { + [Win32Focus]::ShowWindow($AppProcess.MainWindowHandle, 9) | Out-Null + [Win32Focus]::SetForegroundWindow($AppProcess.MainWindowHandle) | Out-Null + Start-Sleep -Milliseconds 500 + [System.Windows.Forms.SendKeys]::SendWait("{TAB}{TAB}{ENTER}") + } + } catch { + Add-Failure "Automatic setup retry keypress failed: $($_.Exception.Message)" + } + + $snapshot = Wait-ForBootstrapPhase ` + (Join-Path $ReportDir "port-conflict") "ready" $TimeoutSeconds $AfterSequence + if ($snapshot -and $snapshot.phase -eq "ready") { + $script:Report.checks.setupRetry = "automatic" + return $true + } + + Write-Host "" + Write-Host "Manual action required: click 'Retry startup' in the setup window." + $snapshot = Wait-ForBootstrapPhase ` + (Join-Path $ReportDir "port-conflict") "ready" 180 $AfterSequence + if ($snapshot -and $snapshot.phase -eq "ready") { + $script:Report.checks.setupRetry = "manual" + return $true + } + return $false +} + +function Invoke-PortConflictScenario([string]$PackageRoot) { + Write-Step "Port conflict recovery scenario" + Stop-GenericAgent + Stop-BridgeOnPort + Start-Sleep -Seconds 2 + + $listener = Start-ForeignPortListener + $script:Report.checks.foreignListenerPid = $listener.Id + $appProc = Start-GenericAgent $PackageRoot "port-conflict" + Start-Sleep -Seconds 2 + + $failed = Wait-ForBootstrapPhase (Join-Path $ReportDir "port-conflict") "failed" 80 + if (-not $failed -or $failed.phase -ne "failed") { + Stop-ProcessTreeSafe $listener.Id + Fail "Port conflict scenario did not reach failed phase" + } + if ($failed.failure.code -ne "port_conflict") { + Stop-ProcessTreeSafe $listener.Id + Fail "Expected port_conflict, got $($failed.failure.code)" + } + try { + Get-Process -Id $listener.Id -ErrorAction Stop | Out-Null + $script:Report.checks.foreignListenerSurvived = $true + } catch { + Fail "Foreign listener was not alive after port_conflict" + } + Capture-Screenshot "setup-failure.png" | Out-Null + + Stop-ProcessTreeSafe $listener.Id + Start-Sleep -Seconds 2 + if (-not (Invoke-SetupRetry $appProc 40 ([int]$failed.seq))) { + Fail "Setup retry did not reach ready after releasing port" + } + Capture-Screenshot "setup-retry-ready.png" | Out-Null + $script:Report.checks.portConflictRecovery = $true +} + +try { + Ensure-Dir $WorkDir + Ensure-Dir $ReportDir + Ensure-Dir $ScreensDir + $Report.environment = Get-WindowsEnvironment + + if (-not $PackageZip) { + Write-Step "Read GitHub Actions run metadata" + Get-RunMetadata | Out-Null + } elseif ($ExpectedCommit) { + $Report.run = [ordered]@{ headSha = $ExpectedCommit; source = "local-package" } + } + + $zip = Resolve-PackageZip + $Report.package.zip = $zip + Test-Sha256 $zip + $packageRoot = Expand-Package $zip + Assert-PackageShape $packageRoot + $Report.environment.initialPortState = @(Get-PortState) + $Report.environment.initialProcesses = @(Get-GaProcesses) + + if ($Mode -eq "Smoke" -or $Mode -eq "Full") { + Invoke-FreshLaunch $packageRoot + } + if ($Mode -eq "FailureOnly" -or $Mode -eq "Full") { + Invoke-PortConflictScenario $packageRoot + } + + $Report.environment.finalPortState = @(Get-PortState) + $Report.environment.finalProcesses = @(Get-GaProcesses) + $Report.success = $true + $script:RunSucceeded = $true + Save-Report + Write-Host "" + Write-Host "E2E report: $ReportPath" +} catch { + $Report.success = $false + Add-Failure $_.Exception.Message + $Report.environment.finalPortState = @(Get-PortState) + $Report.environment.finalProcesses = @(Get-GaProcesses) + Save-Report + Write-Error $_.Exception.Message + exit 1 +} finally { + if ($script:RunSucceeded -and -not $KeepWorkDir) { + Remove-Item -LiteralPath $ExtractDir -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $DownloadDir -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "Report kept at: $ReportDir" + } else { + Write-Host "WorkDir kept for diagnostics: $WorkDir" + } +} diff --git a/frontends/desktop/e2e/windows/README.md b/frontends/desktop/e2e/windows/README.md new file mode 100644 index 000000000..dde567059 --- /dev/null +++ b/frontends/desktop/e2e/windows/README.md @@ -0,0 +1,51 @@ +# Windows Desktop E2E + +This folder contains the Windows validation harness for the portable desktop package. It simulates the user path: + +1. Download a GitHub Actions artifact, or use a local zip. +2. Verify commit, SHA-256, and required package files. +3. Extract to a clean directory. +4. Launch `GenericAgent.exe` directly. +5. Wait for first-run prepare, bridge identity, and bootstrap `ready`. +6. In `Full` mode, inject an unknown process on port `14168`, verify `port_conflict`, release it, and retry from setup. + +## Full Run + +```powershell +.\frontends\desktop\e2e\windows\Invoke-WindowsUserJourney.ps1 ` + -Repo abraxas914/GenericAgent ` + -RunId 29071095889 ` + -ExpectedCommit 696ddfc ` + -Mode Full +``` + +Use `-PackageZip C:\path\GenericAgent-Desktop-Windows-Portable.zip` when the artifact has expired or has already been downloaded. + +## Modes + +- `Smoke`: package verification, extraction, first launch, prepare marker, bridge identity, bootstrap ready. +- `FailureOnly`: assumes the package can be extracted and focuses on the unknown port conflict and setup retry path. +- `Full`: runs `Smoke`, then the failure path. + +## Manual Checks + +The script collects screenshots and writes these checklist items to the report for the tester to mark externally: + +- Loading, prepare, setup, and main windows always show the Windows titlebar. +- Right side has exactly minimize, maximize, and close. +- Blank titlebar area drags the window; button area does not. +- Minimize works. +- Maximize and restore work. +- Close hides to tray instead of exiting. +- Sidebar nav sits directly below the custom titlebar with no blank row. + +## Report + +Reports are written under `\report`: + +- `e2e-report.json` +- `bootstrap-events.jsonl` +- `bootstrap-latest.json` +- screenshots such as `loading-first.png`, `main-ready.png`, and `setup-failure.png` + +Failures exit non-zero and keep diagnostics unless `-KeepWorkDir` is omitted and cleanup succeeds. diff --git a/frontends/desktop/e2e/windows/fixtures/foreign-port-listener.ps1 b/frontends/desktop/e2e/windows/fixtures/foreign-port-listener.ps1 new file mode 100644 index 000000000..fe90a8a7c --- /dev/null +++ b/frontends/desktop/e2e/windows/fixtures/foreign-port-listener.ps1 @@ -0,0 +1,35 @@ +param( + [int]$Port = 14168, + [string]$ReadyFile = "" +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 2.0 + +$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), $Port) +$listener.Start() + +if ($ReadyFile) { + $dir = Split-Path -Parent $ReadyFile + if ($dir) { + [System.IO.Directory]::CreateDirectory($dir) | Out-Null + } + [System.IO.File]::WriteAllText($ReadyFile, "ready", [System.Text.Encoding]::ASCII) +} + +try { + while ($true) { + $client = $listener.AcceptTcpClient() + try { + $stream = $client.GetStream() + $body = "foreign listener" + $response = "HTTP/1.1 404 Not Found`r`nContent-Type: text/plain`r`nContent-Length: $($body.Length)`r`nConnection: close`r`n`r`n$body" + $bytes = [System.Text.Encoding]::ASCII.GetBytes($response) + $stream.Write($bytes, 0, $bytes.Length) + } finally { + $client.Close() + } + } +} finally { + $listener.Stop() +} diff --git a/frontends/desktop/index.html b/frontends/desktop/index.html new file mode 100644 index 000000000..d5b5155e2 --- /dev/null +++ b/frontends/desktop/index.html @@ -0,0 +1,31 @@ + + + + + + +GenericAgent + + + +
    + + + diff --git a/frontends/desktop/loading.html b/frontends/desktop/loading.html new file mode 100644 index 000000000..97d524d31 --- /dev/null +++ b/frontends/desktop/loading.html @@ -0,0 +1,21 @@ + + + + +GenericAgent + + + +
    +
    +
    + + + diff --git a/frontends/desktop/package-lock.json b/frontends/desktop/package-lock.json new file mode 100644 index 000000000..aa5fe6403 --- /dev/null +++ b/frontends/desktop/package-lock.json @@ -0,0 +1,13787 @@ +{ + "name": "genericagent-desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "genericagent-desktop", + "version": "0.1.0", + "dependencies": { + "@douyinfe/semi-icons": "^2.71.1", + "@douyinfe/semi-ui": "^2.71.1", + "@lobehub/icons": "^5.10.1", + "@types/prismjs": "^1.26.6", + "@vscode/codicons": "^0.0.45", + "katex": "^0.17.0", + "prismjs": "^1.30.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "rehype-katex": "^7.0.1", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "@wdio/cli": "^9.29.1", + "@wdio/local-runner": "^9.29.1", + "@wdio/mocha-framework": "^9.29.1", + "@wdio/spec-reporter": "^9.29.1", + "@wdio/tauri-plugin": "^1.2.0", + "@wdio/tauri-service": "^1.2.0", + "happy-dom": "^20.10.6", + "tsx": "^4.23.1", + "typescript": "^5.6.3", + "vite": "^6.0.5", + "vitest": "^4.1.9", + "webdriverio": "^9.29.1" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ant-design/cssinjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", + "integrity": "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", + "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.7", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@douyinfe/semi-animation": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@douyinfe/semi-animation/-/semi-animation-2.101.0.tgz", + "integrity": "sha512-dcESvnz0vHdxc2v0NnxCRFVedT88r250x0xPJ1x9BMnUn/PCSN9BtAUl2hktoLVyhp/+So41ThPvn/2tTAvqVA==", + "license": "MIT", + "dependencies": { + "bezier-easing": "^2.1.0" + } + }, + "node_modules/@douyinfe/semi-animation-react": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@douyinfe/semi-animation-react/-/semi-animation-react-2.101.0.tgz", + "integrity": "sha512-TTUvFBYpeABxt2ZYFXmz908c23ZwDXuQJGZlBXxTb2yLFfFtSpXyctKzG94cxGcT0ZCliOlE3xQ2FhVT0mej0g==", + "license": "MIT", + "dependencies": { + "@douyinfe/semi-animation": "2.101.0", + "@douyinfe/semi-animation-styled": "2.101.0", + "classnames": "^2.2.6" + } + }, + "node_modules/@douyinfe/semi-animation-styled": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.101.0.tgz", + "integrity": "sha512-FM3xOP+t2kqcHT7x7XSTkYf6sIgsd3KXfE5K2uIUxaRm42aBgRS1xP/5PT35U3IBOlP8pi9st7GZiYESHfHfYw==", + "license": "MIT" + }, + "node_modules/@douyinfe/semi-foundation": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@douyinfe/semi-foundation/-/semi-foundation-2.101.0.tgz", + "integrity": "sha512-ESEH7aCX3IETSmp952Lq2f4lxApYn2uH72YJ4WUJ5YoOMxtWZCFVf85R02+zBlEP/dTED9PBl3aOazkgdH/USA==", + "license": "MIT", + "dependencies": { + "@douyinfe/semi-animation": "2.101.0", + "@douyinfe/semi-json-viewer-core": "2.101.0", + "@mdx-js/mdx": "^3.0.1", + "async-validator": "^3.5.0", + "classnames": "^2.2.6", + "date-fns": "^2.29.3", + "date-fns-tz": "^1.3.8", + "fast-copy": "^3.0.1 ", + "lodash": "^4.17.21", + "lottie-web": "^5.13.0", + "memoize-one": "^5.2.1", + "prismjs": "^1.29.0", + "remark-gfm": "^4.0.0", + "scroll-into-view-if-needed": "^2.2.24" + } + }, + "node_modules/@douyinfe/semi-icons": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@douyinfe/semi-icons/-/semi-icons-2.101.0.tgz", + "integrity": "sha512-BFDALkxYbjZmFdpcTYgE031uWJnWC/s7olR8j8F21RM1W2LIVCnpGFXLAppUj+y+5xeG2wcL57MA9AxBvPa0aw==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.6" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/@douyinfe/semi-illustrations": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@douyinfe/semi-illustrations/-/semi-illustrations-2.101.0.tgz", + "integrity": "sha512-iVFqwaftoMRpnAeeYoAOD7+06plRLW4HqyufVPpTwPBkdalANoX5NtdGPwc4w9E4+O+pu8vMp1snojhVrxYIMw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/@douyinfe/semi-json-viewer-core": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@douyinfe/semi-json-viewer-core/-/semi-json-viewer-core-2.101.0.tgz", + "integrity": "sha512-0Jp4/3Go0kQnlYIi+NzM3Dh1/YJjMjXGPQV9WfjlZXAE47FIM+KTNWL8wyGQz5x91HKV0ZABd7pGw76abODT3g==", + "license": "MIT", + "dependencies": { + "jsonc-parser": "^3.3.1" + } + }, + "node_modules/@douyinfe/semi-theme-default": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@douyinfe/semi-theme-default/-/semi-theme-default-2.101.0.tgz", + "integrity": "sha512-TZHhGapKNtd06eGb+RDpntmTo1iKsh/2KQgH4YDzrEnuCs59EGEj4Uh+RkhlMLGWkxR/qrP6weMnl8K3k/E69g==", + "license": "MIT" + }, + "node_modules/@douyinfe/semi-ui": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@douyinfe/semi-ui/-/semi-ui-2.101.0.tgz", + "integrity": "sha512-oTIE9Hrc++8zv4MSanbkMlFpqoQeT/Gn7fPv+B1b1HJZIO0Dzj7pAimv2BBoZ4mB8QcsJ2BXFALg/Lp3ienbqA==", + "license": "MIT", + "dependencies": { + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", + "@douyinfe/semi-animation": "2.101.0", + "@douyinfe/semi-animation-react": "2.101.0", + "@douyinfe/semi-foundation": "2.101.0", + "@douyinfe/semi-icons": "2.101.0", + "@douyinfe/semi-illustrations": "2.101.0", + "@douyinfe/semi-theme-default": "2.101.0", + "@tiptap/core": "^3.10.7", + "@tiptap/extension-document": "^3.10.7", + "@tiptap/extension-hard-break": "^3.10.7", + "@tiptap/extension-image": "^3.10.7", + "@tiptap/extension-mention": "^3.10.7", + "@tiptap/extension-paragraph": "^3.10.7", + "@tiptap/extension-text": "^3.10.7", + "@tiptap/extension-text-align": "^3.10.7", + "@tiptap/extension-text-style": "^3.10.7", + "@tiptap/extensions": "^3.10.7", + "@tiptap/pm": "^3.10.7", + "@tiptap/react": "^3.10.7", + "@tiptap/starter-kit": "^3.10.7", + "async-validator": "^3.5.0", + "classnames": "^2.2.6", + "copy-text-to-clipboard": "^2.1.1", + "date-fns": "^2.29.3", + "date-fns-tz": "^1.3.8", + "fast-copy": "^3.0.1 ", + "jsonc-parser": "^3.3.1", + "lodash": "^4.17.21", + "prop-types": "^15.7.2", + "prosemirror-state": "^1.4.3", + "react-resizable": "^3.0.5", + "react-window": "^1.8.2", + "scroll-into-view-if-needed": "^2.2.24", + "utility-types": "^3.10.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache/node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/@emotion/css": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", + "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", + "license": "MIT", + "dependencies": { + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/serialize/node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/serialize/node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT", + "optional": true + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "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-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/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/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types/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/@jest/types/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/@jest/types/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/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lobehub/icons": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/@lobehub/icons/-/icons-5.13.0.tgz", + "integrity": "sha512-iXQF8GFvlwNJMR+PaU3jCgVAn5B8F7P48Fm6aodSXP+b+HJiR266rvlMSYvCULRAB/6/rtS1WZH3npc3p3viFw==", + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "antd-style": "^4.1.0", + "es-toolkit": "^1.49.0", + "lucide-react": "^0.469.0", + "polished": "^4.3.1" + }, + "peerDependencies": { + "@lobehub/ui": "^5.0.0", + "antd": "^6.1.1", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@nodable/entities": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "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/@promptbook/utils": { + "version": "0.69.5", + "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.69.5.tgz", + "integrity": "sha512-xm5Ti/Hp3o4xHrsK9Yy3MS6KbDxYbq485hDsFvxqaNA7equHLPdo8H8faTitTeb14QCDfLW4iwCxdVYu5sn6YQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/webgptorg/promptbook/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "CC-BY-4.0", + "dependencies": { + "spacetrim": "0.11.59" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", + "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rc-component/util": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.12.0.tgz", + "integrity": "sha512-AEjPL8JVdohIITaiXokyjL9WQ6tKWWjAYK9QU16tGNE9JaQABBQy+hA4H2Lup5MgXy9yY3iLrbZJheuU13hTdQ==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^19.2.7" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.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.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-log": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.8.0.tgz", + "integrity": "sha512-a+7rOq3MJwpTOLLKbL8d0qGZ85hgHw5pNOWusA9o3cf7cEgtYHiGY/+O8fj8MvywQIGqFv0da2bYQDlrqLE7rw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tiptap/core": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.28.0.tgz", + "integrity": "sha512-gUuD5WAYfbDxNSSJya/emh2KSzXZXLUYKW4fEnc1AQ5FE2twzh4LJ9UlKFIawigrUCAksWI5Fy1hRbv+5m4ZdQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.28.0" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.28.0.tgz", + "integrity": "sha512-y8Xi6Z3AQLXedz0J8Z3kDsu7SKBmL50gKVXx3RK1Oo1cuCGhNChm3syChkAz3PLAbrPUM5rvJDSZdF2jUrDnng==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0", + "@tiptap/pm": "3.28.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.28.0.tgz", + "integrity": "sha512-JhZQmr0AU741bOwjVMfuwJdK4g0TQwwPbeca9aqKHv5zvZw4i4G9G6fESVyMFc3Yag1ffpnq5EtNldHKTnMhlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.28.0.tgz", + "integrity": "sha512-7AUNoHj2K4XLKCgW4uspeD/ENejPu2BeHvLTsiOoIO+XXDNSi2j0bbaIS8f2s/qnFirscwp/sbznp1qph/76qg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0", + "@tiptap/pm": "3.28.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.28.0.tgz", + "integrity": "sha512-dSVuNiH7PFMmWGkNER9vfUb5bXUHDARvHbJ3l+APEb+ZiusVN1KFdM3nd/RsW/Rt5Gd3XwlIEU/c2Uf7cxYDcw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.28.0" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.28.0.tgz", + "integrity": "sha512-AUw2Acof3CQE6Q6Y22saK4lyg0nkeJ4XXniU65TdAi/9TE66TGiO4NQJJNNPgu8+VMEwl5j8VsIFK8sfTcNYtg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.28.0.tgz", + "integrity": "sha512-bHITDzT0umTLh+SiEVQzIXkCMt/TzbPM1+HQy9ZxgQDHAJj/vdmfNAnP3RCOrz4lETXyhNQ2b6kxeu3lWckxyA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0", + "@tiptap/pm": "3.28.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.28.0.tgz", + "integrity": "sha512-5SAKtlB1Tr/eZFhISL86HqmUup6rItvPtuPyRjmZo/VgbMwXEwiyAh1sMgFp5VNaeSMM14vJSyQpjhSaOmaBqg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.28.0.tgz", + "integrity": "sha512-JA+dXQjjfJBpD0nVsZeddGVXRsmanESM9+8CtM35hI9wJnYMXay/B+5GUhswO20zhT3zhB2ZOTGe9knu6A8nIQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.28.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.28.0.tgz", + "integrity": "sha512-59SECvJq3pQfeJBuydEdhQqrpl0oDlk8N1Ovs1Si3/fJa6rEQAJB2zIvcpBHky9Z+5JodI2eueDnv8eimiyGbg==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.28.0", + "@tiptap/pm": "3.28.0" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.28.0.tgz", + "integrity": "sha512-Wo73kM4q8z4Va9i4+0n1cO0UEMVUVBHPqM6cAqEYXjB4T48LQkKjRsiQydCezm185rQAsmm1dyi72gtvVQfdMg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.28.0" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.28.0.tgz", + "integrity": "sha512-JHIFjX1luJgQ4VFEkIeXZpbc54Qiw+69P8FmlazYTh7AIJ6iLCpMFgQFELnivEAZ+/vS0lMPQubg0rCeM3UrHw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.28.0.tgz", + "integrity": "sha512-rKjGG/Ik2lNaXBNVmONEDm5DBfGDLM1NWgSf5RRfBISrH8Lm1xqFruOB9tIaB0YUoSV3SBOiwTF9svmzvKSoRA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.28.0.tgz", + "integrity": "sha512-neBCMWprkppQUoLWyeJZDHqBw+xKX2oNhLD9MbFlhKIr092zgfP4mpCvQnSkn1OHl156KzZMp070+NyLBjgQ7A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0", + "@tiptap/pm": "3.28.0" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.28.0.tgz", + "integrity": "sha512-aKGCdEjyujUcQ6tEtq4EPbKxEC16QsKapv3DeZs1c/UHFiEUalbbPHBnxjGnupiLz75IR+07GYqBoZ81kSCxiA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.28.0.tgz", + "integrity": "sha512-Yur/ELz6dNVKQC7m8wGjtoLFxamGjJmA0rUEEOacTH6J39aFmcSxYkEGNDkYHbZFyHFYqbej6eI3VE0h08O0mg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.28.0.tgz", + "integrity": "sha512-hy/PqSeTyl317yseFcHGE+3XVfQKQYaL4uZuj27xlrcO7MZ15drt1h9sUZy1FTBF3mr8676pQu7aoEm/Ww4ZRA==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0", + "@tiptap/pm": "3.28.0" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.28.0.tgz", + "integrity": "sha512-zQ66i5DuhVOndmZ0d7H475Y5Yb+BMx+zfCjFkCzEc6WVef5MumtxBVTkGLQ0ibxUaD71s4QQZbjHWagpaTg0eQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0", + "@tiptap/pm": "3.28.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.28.0.tgz", + "integrity": "sha512-GbXrnMac6knSetZY33NHwOrgXFPqH28uCklQqmOZQlsrdGqezDKk31qoEMr4GsKW9n/lViAeuc07oIzwLwCMng==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.28.0" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.28.0.tgz", + "integrity": "sha512-u9Wks8c/eQAv0RkULNggOyTKDD69WVbcV9H5cbasPDUbq5XbEFVa9ajxhEGfMmQ5et3EX0QL8qBi50K0GqbIjA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.28.0" + } + }, + "node_modules/@tiptap/extension-mention": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-3.28.0.tgz", + "integrity": "sha512-Y3TJOTLpTCLmUJuKGA5K8Rxl5GpgwVd7atda0yCmKKYO0czTL4afN5tNIUSWoBqj0nBQdi4DU+Xnl+s+EfM/4g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0", + "@tiptap/pm": "3.28.0", + "@tiptap/suggestion": "3.28.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.28.0.tgz", + "integrity": "sha512-OZNYrKKL9D01ySOujlNbKlLS9/3wGgKNYeoHZUg0e+Ta8WPVvL3W1zH6TgiU5sF2ZSGUBoq3w7mdaO/iROlUkw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.28.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.28.0.tgz", + "integrity": "sha512-8UMlQIjzqf6r2hSJ/VeJ00RqaOrOB1J5rTk02Vcw2pYeHkOqp+B0ff0HFu4abT9x1q4oXrBAgMsxRtgh/kR14Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.28.0.tgz", + "integrity": "sha512-VVj2ZZU9QYqiHLcjqMqRYvuHSsokj/AgUl+6TzLrKjlWwyZ18D4H2vkyI0g70iVTKnUpZ3sEy8WA/rqjgBjqBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.28.0.tgz", + "integrity": "sha512-Jqp1LgfY1mnp4TQHoy+vnHyfn6qnnAM6nHgGwbY5zA8b/Xf1Etll6pziTx9p1J1qcfAgSrnjOyAmA++H7VQqww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-text-align": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.28.0.tgz", + "integrity": "sha512-P+KQMTtCpLFq9dRvy/fbyu2BGALulT/1HtsLbpFG4tKATaKa+cFnJ7rLZF7b3AdAMDNtXdnivxAtixAMBt2QWw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.28.0.tgz", + "integrity": "sha512-UreMyqZSv770+CXJK/l1qeHUdaNMQAVkxSOPWpiY9FiT8B4OQyjA7WBOlWrzbvVTzfJvXWk2Y3SWgVRx+/Cd9A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.28.0.tgz", + "integrity": "sha512-rwxCS6vTh2DJkNIYQX7JSrrRSmm76e2y48aZeuZO5ShkvLWMuJ3/zqDHRxAQVDiJhuE2Cp8NrTmPYlUc9YrT0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.28.0.tgz", + "integrity": "sha512-DJT1khCK+O/pT1gQlAnoKAx6zwDkgv7GtnhSfkqm1/4KHC3x+SSJnBIgBl9oGHymA+DxWbW1EULU4V3d/kT2uQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0", + "@tiptap/pm": "3.28.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.28.0.tgz", + "integrity": "sha512-ALcpwZMUdat9gjJKlpscpoqXStoLhU246LPEVBDvJdIsoUKvUu3MrzfXik2Y8mtSGfhjtm9O2TRkWxQiFVMwsQ==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.4.1", + "prosemirror-commands": "^1.7.1", + "prosemirror-dropcursor": "^1.8.2", + "prosemirror-gapcursor": "^1.4.1", + "prosemirror-history": "^1.5.0", + "prosemirror-inputrules": "^1.5.1", + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.9", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.5", + "prosemirror-transform": "^1.12.0", + "prosemirror-view": "^1.41.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.28.0.tgz", + "integrity": "sha512-BxzSAqaDEldQ97K/v6+GizU1/Dxx9VWkRh7PLQql6bXlAMiz6T1G3dGt25vvv1hQ1FoMt5ExWY5NkrPmR8G8ew==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.28.0", + "@tiptap/extension-floating-menu": "^3.28.0" + }, + "peerDependencies": { + "@tiptap/core": "3.28.0", + "@tiptap/pm": "3.28.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.28.0.tgz", + "integrity": "sha512-pqvFpValV+ONtlu3l4s73ROm/tEjoGMmt6uHmSJaLbqTcMHkdWuR3flJ/5brr4IDHe1foxmNicMbRxlje7yBYw==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.28.0", + "@tiptap/extension-blockquote": "^3.28.0", + "@tiptap/extension-bold": "^3.28.0", + "@tiptap/extension-bullet-list": "^3.28.0", + "@tiptap/extension-code": "^3.28.0", + "@tiptap/extension-code-block": "^3.28.0", + "@tiptap/extension-document": "^3.28.0", + "@tiptap/extension-dropcursor": "^3.28.0", + "@tiptap/extension-gapcursor": "^3.28.0", + "@tiptap/extension-hard-break": "^3.28.0", + "@tiptap/extension-heading": "^3.28.0", + "@tiptap/extension-horizontal-rule": "^3.28.0", + "@tiptap/extension-italic": "^3.28.0", + "@tiptap/extension-link": "^3.28.0", + "@tiptap/extension-list": "^3.28.0", + "@tiptap/extension-list-item": "^3.28.0", + "@tiptap/extension-list-keymap": "^3.28.0", + "@tiptap/extension-ordered-list": "^3.28.0", + "@tiptap/extension-paragraph": "^3.28.0", + "@tiptap/extension-strike": "^3.28.0", + "@tiptap/extension-text": "^3.28.0", + "@tiptap/extension-underline": "^3.28.0", + "@tiptap/extensions": "^3.28.0", + "@tiptap/pm": "^3.28.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "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/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "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==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": 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/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", + "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "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/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vscode/codicons": { + "version": "0.0.45", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45.tgz", + "integrity": "sha512-1KAZ7XCMagp5Gdrlr4bbbcAqgcIL623iO1wW6rfcSVGAVUQvR0WP7bQx1SbJ11gmV3fdQTSEFIJQ/5C+HuVasw==", + "license": "CC-BY-4.0" + }, + "node_modules/@wdio/cli": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.29.1.tgz", + "integrity": "sha512-MjRHdM5mGuibhwv+pu8rJD1Pxl6JVj4Pvy9stuj/SjbTqWRxjLauLd3i7+tIMdmrK0ajGO0n249HT8vS8Mh0Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/snapshot": "^2.1.1", + "@wdio/config": "9.29.1", + "@wdio/globals": "9.29.1", + "@wdio/logger": "9.29.1", + "@wdio/protocols": "9.29.1", + "@wdio/types": "9.29.1", + "@wdio/utils": "9.29.1", + "async-exit-hook": "^2.0.1", + "chalk": "^5.4.1", + "chokidar": "^4.0.0", + "create-wdio": "9.29.1", + "dotenv": "^17.2.0", + "import-meta-resolve": "^4.0.0", + "lodash.flattendeep": "^4.4.0", + "lodash.pickby": "^4.6.0", + "lodash.union": "^4.6.0", + "read-pkg-up": "^10.0.0", + "tsx": "^4.7.2", + "webdriverio": "9.29.1", + "yargs": "^17.7.2" + }, + "bin": { + "wdio": "bin/wdio.js" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/config": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.29.1.tgz", + "integrity": "sha512-8IXDiRG9wUUnpU6M/uzsVqIHJD/7o4y9RaOU2Jh/OdRJP/7rxwfGsa24Bv486rnMGdghztkwLCBJWG0jbeEtfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "9.29.1", + "@wdio/types": "9.29.1", + "@wdio/utils": "9.29.1", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0", + "jiti": "^2.6.1" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/dot-reporter": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/dot-reporter/-/dot-reporter-9.29.1.tgz", + "integrity": "sha512-5UVgxKHVoJfJVSg3VH9n0yPkstHzX65g5zRBtNX51hqXmSPRE9kSLGq53Lo91UTORc0og3i0UT93zZqEQhpzjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/reporter": "9.29.1", + "@wdio/types": "9.29.1", + "chalk": "^5.0.1" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/globals": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.29.1.tgz", + "integrity": "sha512-F96BKppx4HGD64v+s57TM4K4zaxqUCg2RXHk6sjB2xrSa7P+d2VYV28ID2ddF7iSodVfPlpa1i2/jy8jMGrU8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "expect-webdriverio": "^5.6.5", + "webdriverio": "^9.0.0" + }, + "peerDependenciesMeta": { + "expect-webdriverio": { + "optional": false + }, + "webdriverio": { + "optional": false + } + } + }, + "node_modules/@wdio/local-runner": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-9.29.1.tgz", + "integrity": "sha512-ypgi3gTZYY3eStp1U9H6Gjl0mer55CYMdbtRQ27s6evN1QABcwkGCTuzwqHoO6RmKzwPOuht2mnWb9HwYJaNnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@wdio/logger": "9.29.1", + "@wdio/repl": "9.16.2", + "@wdio/runner": "9.29.1", + "@wdio/types": "9.29.1", + "@wdio/xvfb": "9.29.1", + "exit-hook": "^4.0.0", + "expect-webdriverio": "^5.6.5", + "split2": "^4.1.0", + "stream-buffers": "^3.0.2" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/logger": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.29.1.tgz", + "integrity": "sha512-0ZAEIo6PNyMIJPlOGkIgyOJUjcd0pC8/QHlVAAe1c91/IcjZ1X+k0yidXHaboJdN7dq1XPUacmhRdtua0U5EZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/mocha-framework": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-9.29.1.tgz", + "integrity": "sha512-GG3z0OtD2eu15ql3vnI6VcLJ3INUfSrqbuop/tCAbCjkkKgpjq1JOno0lrDNKRt5Ndq4a4/c9Wu5uxfL+CItYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mocha": "^10.0.6", + "@types/node": "^20.11.28", + "@wdio/logger": "9.29.1", + "@wdio/types": "9.29.1", + "@wdio/utils": "9.29.1", + "mocha": "^10.3.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/native-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@wdio/native-core/-/native-core-1.0.0.tgz", + "integrity": "sha512-SBVipUZk1+fiwwyGkDjp0gZsV+AGlZ1NE3rPviPCzs6QKm6Qmj7di/05P5MdqYUq/PslrVEWNIp6TYfL+wjEKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "9.18.0", + "@wdio/native-types": "2.3.1", + "@wdio/native-utils": "2.4.0", + "debug": "^4.4.3", + "get-port": "^7.1.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.11.0" + } + }, + "node_modules/@wdio/native-core/node_modules/@wdio/logger": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/native-core/node_modules/@wdio/native-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@wdio/native-types/-/native-types-2.3.1.tgz", + "integrity": "sha512-q6BOAd7Yg8lQJJWW/z1a4ratTttMIy39MfbAIgw8PKrXB6Oc+x95KXkuIcCGDINTk+X92Lbjxefiww4jiPam8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.11.0" + } + }, + "node_modules/@wdio/native-spy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wdio/native-spy/-/native-spy-1.1.0.tgz", + "integrity": "sha512-1BTbiWo9fNOsnSRYTjBE0Z6PtknFMtN7/TH2SsnishkIABUHH8ONKB+NWiTCiX8IkFiARguo2bJHEQBOULXlwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.11.0" + } + }, + "node_modules/@wdio/native-types": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@wdio/native-types/-/native-types-2.4.0.tgz", + "integrity": "sha512-GjgPskOoTvl17Jwj8aAU3A31gpklL5M1KfbrwYffbQT16CTpJTD8A4PdoS2ZjGoCWcyLBR1CWdJI3W8T0JBLjw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.11.0" + } + }, + "node_modules/@wdio/native-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@wdio/native-utils/-/native-utils-2.5.0.tgz", + "integrity": "sha512-Qv0mJ2elRBZCgYbHCnKPNgE9VaqnmNJyjythTDYyOQpj8pAs6FZXXmWXGkUIdMds5cK0ux/qtxyKU0Y0tDsUUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "9.18.0", + "debug": "^4.4.3", + "esbuild": "^0.28.0", + "find-up-simple": "^1.0.1", + "json5": "^2.2.3", + "smol-toml": "^1.6.0", + "yaml": "^2.9.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.11.0" + }, + "peerDependencies": { + "tsx": "^4.22.4" + }, + "peerDependenciesMeta": { + "tsx": { + "optional": true + } + } + }, + "node_modules/@wdio/native-utils/node_modules/@wdio/logger": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/protocols": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.29.1.tgz", + "integrity": "sha512-NFlBQOA4zDb4D/ETpVMqDgbJyEqdhGRsJWybLOXG7PGlPwfcrfmTMHC1+Boq4KODgpwbhCmI9zIk2JQmGYIttQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/repl": { + "version": "9.16.2", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.16.2.tgz", + "integrity": "sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/reporter": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-9.29.1.tgz", + "integrity": "sha512-CKAcVy9BGwvufokMcl3H+yNcvD11Klku/1BPz8bKSRv7/KzueXR06s1cPbJH3tv9r9zxPErxgdVMpulN8I0qAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@wdio/logger": "9.29.1", + "@wdio/types": "9.29.1", + "diff": "^8.0.2", + "object-inspect": "^1.12.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/runner": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-9.29.1.tgz", + "integrity": "sha512-LV9+0U74J1YModG0T64Kdnh+xvOcd9MWGFlKyXnCtfFDV+47K9BpoDMrc/ng3daLrIKcZGVbym+GhEShiM0DPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.11.28", + "@wdio/config": "9.29.1", + "@wdio/dot-reporter": "9.29.1", + "@wdio/globals": "9.29.1", + "@wdio/logger": "9.29.1", + "@wdio/types": "9.29.1", + "@wdio/utils": "9.29.1", + "deepmerge-ts": "^7.0.3", + "webdriver": "9.29.1", + "webdriverio": "9.29.1" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "expect-webdriverio": "^5.6.5", + "webdriverio": "^9.0.0" + }, + "peerDependenciesMeta": { + "expect-webdriverio": { + "optional": false + }, + "webdriverio": { + "optional": false + } + } + }, + "node_modules/@wdio/spec-reporter": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-9.29.1.tgz", + "integrity": "sha512-iqlHW/qGDRGmxQKN7XyCHxfKphTbPNnniV3yxNXZRSGHCCQok5KFKOnj4fpUCKEyD5jtPAmP6Y0qc1UyR3Dc3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/reporter": "9.29.1", + "@wdio/types": "9.29.1", + "chalk": "^5.1.2", + "easy-table": "^1.2.0", + "pretty-ms": "^9.0.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/tauri-plugin": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@wdio/tauri-plugin/-/tauri-plugin-1.2.0.tgz", + "integrity": "sha512-qeQQ4D0Q4BkwC+Cyez1I343DRm9UErCXksZ/DN1qCpJCorEEPKL3IiYb34OtNJBgnbhqwtGukyjOZYvnpPz95A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tauri-apps/api": "2.11.0", + "@tauri-apps/plugin-log": "2.8.0", + "@wdio/native-spy": "1.1.0", + "@wdio/native-utils": "2.4.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.11.0" + } + }, + "node_modules/@wdio/tauri-service": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@wdio/tauri-service/-/tauri-service-1.2.0.tgz", + "integrity": "sha512-/PEmbDKra6Lsodes5x1Sg5H98bG4hObYZWMyrlbv0e5fL0KVMQMxDP798jWWIqT6+iOPsIlkLirRJKfXXGVtsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/globals": "9.27.1", + "@wdio/logger": "9.18.0", + "@wdio/native-core": "1.0.0", + "@wdio/native-spy": "1.1.0", + "@wdio/native-types": "2.4.0", + "@wdio/native-utils": "2.4.0", + "@wdio/spec-reporter": "9.27.1", + "@wdio/types": "9.27.1", + "debug": "^4.4.3", + "get-port": "^7.1.0", + "tslib": "^2.8.1", + "webdriverio": "9.27.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.11.0" + }, + "peerDependencies": { + "webdriverio": "^9.0.0" + }, + "peerDependenciesMeta": { + "webdriverio": { + "optional": false + } + } + }, + "node_modules/@wdio/tauri-service/node_modules/@wdio/config": { + "version": "9.27.1", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.27.1.tgz", + "integrity": "sha512-QVfSCqcpMfVum9KlpxgjaLlSLXkc53UQ2CPJU+IUVBp8LkbSyeX972HQS8V9Hnn6vSPE1dYScItg7wblnJ8RQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "9.18.0", + "@wdio/types": "9.27.1", + "@wdio/utils": "9.27.1", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0", + "jiti": "^2.6.1" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/tauri-service/node_modules/@wdio/globals": { + "version": "9.27.1", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.27.1.tgz", + "integrity": "sha512-jm6gTQ6Qo3EOBY6PA09U/5Pf17WLEJM1/lTfhc6jzLFE770EuhuhbuphqrInH0hVR9WMyWtSZQ+LRCcLfcmOPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "expect-webdriverio": "^5.6.5", + "webdriverio": "^9.0.0" + }, + "peerDependenciesMeta": { + "expect-webdriverio": { + "optional": false + }, + "webdriverio": { + "optional": false + } + } + }, + "node_modules/@wdio/tauri-service/node_modules/@wdio/logger": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", + "integrity": "sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "safe-regex2": "^5.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/tauri-service/node_modules/@wdio/protocols": { + "version": "9.27.1", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.27.1.tgz", + "integrity": "sha512-Ril46AmySoiYX9nuKqFr3SNJqquU3VmF9FzSndQlDib0G3oA4pYx9wcBXvdvkFxRjjmFwQDzmvztKrssAHymgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/tauri-service/node_modules/@wdio/reporter": { + "version": "9.27.1", + "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-9.27.1.tgz", + "integrity": "sha512-2ueVjd5hOCclfC+GV3yhaN/4Tids1mXMcpPtNTPushHIQY4gLmBqqKDe5RSXAED3bNU+DRdHq2uBiZTBd4QDJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.27.1", + "diff": "^8.0.2", + "object-inspect": "^1.12.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/tauri-service/node_modules/@wdio/spec-reporter": { + "version": "9.27.1", + "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-9.27.1.tgz", + "integrity": "sha512-q9UMJJbCcP+nCOojIvOIcsXnerhHICmWu94guRMRYPbW2IsG/5VM/uhzwru8SU/1WRXLtKgTjkuXXsjzjVpf2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/reporter": "9.27.1", + "@wdio/types": "9.27.1", + "chalk": "^5.1.2", + "easy-table": "^1.2.0", + "pretty-ms": "^9.0.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/tauri-service/node_modules/@wdio/types": { + "version": "9.27.1", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.27.1.tgz", + "integrity": "sha512-EHBNCvLmvpYerln4mb/OBxzKtnavL2wdenjhwuYjzkZMOWHgm/uLXH6sLThM0y6DIbCU72Asth16fo1eDcsofA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/tauri-service/node_modules/@wdio/utils": { + "version": "9.27.1", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.27.1.tgz", + "integrity": "sha512-s2w1tFrvmpdkZ33LYsIw4ONRdWIIm4MxkyIuibbcG1ILV5fFMS9rU59csHuWIM0KhJoEoLU+fzE3ze9O7TpWhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.18.0", + "@wdio/types": "9.27.1", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.2", + "geckodriver": "^6.1.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/tauri-service/node_modules/webdriver": { + "version": "9.27.1", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.27.1.tgz", + "integrity": "sha512-vr6h+RNQ75O2cofgVrdupGxtKjPEBaBYx/lHCHe0giJfAK01oL0U/yrOksJi7kmpev/daN93ldFPhlIlmWtv8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.27.1", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.27.1", + "@wdio/types": "9.27.1", + "@wdio/utils": "9.27.1", + "deepmerge-ts": "^7.0.3", + "https-proxy-agent": "^7.0.6", + "undici": "^6.21.3", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/tauri-service/node_modules/webdriverio": { + "version": "9.27.1", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.27.1.tgz", + "integrity": "sha512-iPaIU/DluYY7zfLiwXDdoLU/6ZW8eup4PNwQikrCzTfvH/ITllRhFUe6NRDTEEePSxxRTeXAn9nehCs98xWGVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.27.1", + "@wdio/logger": "9.18.0", + "@wdio/protocols": "9.27.1", + "@wdio/repl": "9.16.2", + "@wdio/types": "9.27.1", + "@wdio/utils": "9.27.1", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.8.1", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^12.0.0", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.27.1" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": ">=22.x || <=24.x" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/@wdio/types": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.29.1.tgz", + "integrity": "sha512-jp8jgMv6TS35G96YzHZxw3PVN0Dz6xQ6tnMAicndAJ8Jt9AIXb0ywIse4TjaFywakO3dLoEhlyA3ZtR26vmt+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/utils": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.29.1.tgz", + "integrity": "sha512-jyt6b6FfdYwVbMISVhuyGC1xQGZj6xM03KhTHozH7a9/zu9b++94KRdT9HRbwX8zefjW0YeIC5+qWSIf7WWHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.29.1", + "@wdio/types": "9.29.1", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.2", + "geckodriver": "^6.1.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "mitt": "^3.0.1", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/xvfb": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/@wdio/xvfb/-/xvfb-9.29.1.tgz", + "integrity": "sha512-suC0EJcPVldpIyJA/UB9cV11PkW4sGgNmLHPhWU7NiASnbiFeHjK3EbevjzlwwBNYXx1KHO4jMY4Rep2wwVNuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "9.29.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@zip.js/zip.js": { + "version": "2.8.31", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.31.tgz", + "integrity": "sha512-2NLmow9ax/5IdBbJKxNQp3Ur9mNxmgLfN2Yp/FKNh1ZIGs3OYkiC3AIUfIZouTPSgeW5+F1bzTAZAyD2Y4ZfFA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=18.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antd-style": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/antd-style/-/antd-style-4.1.0.tgz", + "integrity": "sha512-vnPBGg0OVlSz90KRYZhxd89aZiOImTiesF+9MQqN8jsLGZUQTjbP04X9jTdEfsztKUuMbBWg/RmB/wHTakbtMQ==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.0.0", + "@babel/runtime": "^7.24.1", + "@emotion/cache": "^11.11.0", + "@emotion/css": "^11.11.2", + "@emotion/react": "^11.11.4", + "@emotion/serialize": "^1.1.3", + "@emotion/utils": "^1.2.1", + "use-merge-value": "^1.2.0" + }, + "peerDependencies": { + "antd": ">=6.0.0", + "react": ">=18" + } + }, + "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/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "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/async-validator": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-3.5.2.tgz", + "integrity": "sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ==", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/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/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz", + "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "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.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bezier-easing": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", + "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==", + "license": "MIT" + }, + "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/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/cheerio/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/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", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", + "license": "MIT" + }, + "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/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/copy-text-to-clipboard": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-2.2.0.tgz", + "integrity": "sha512-WRvoIdnTs1rgPMkgA2pUOa/M4Enh2uzCwdKsOMYNAJiz/4ZvEJgmbF4OmninPmlFdAWisfeh0tH+Cpf7ni3RqQ==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "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", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/create-wdio": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/create-wdio/-/create-wdio-9.29.1.tgz", + "integrity": "sha512-EWef5jtJ9pN+tYblwZvWwDEKEgZSMy8VZCUCWBIiw3Z48aDbqusxpOseZWZ/rAttsqGyntzIvHLxIo0r7kryxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^14.0.0", + "cross-spawn": "^7.0.3", + "ejs": "^3.1.10", + "execa": "^9.6.0", + "import-meta-resolve": "^4.1.0", + "inquirer": "^12.7.0", + "normalize-package-data": "^7.0.0", + "read-pkg-up": "^10.1.0", + "recursive-readdir": "^2.2.3", + "semver": "^7.6.3", + "type-fest": "^4.41.0", + "yargs": "^17.7.2" + }, + "bin": { + "create-wdio": "bin/wdio.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/create-wdio/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "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/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-shorthand-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.2.tgz", + "integrity": "sha512-C2AugXIpRGQTxaCW0N7n5jD/p5irUmCrwl03TrnMFBHDbdq44CFWR2zO7rK9xPN4Eo3pUxC4vQzQgbIpzrD1PQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-value": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", + "dev": true + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/date-fns-tz": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz", + "integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": ">=2.0.0" + } + }, + "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/decamelize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.1.tgz", + "integrity": "sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "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", + "optional": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/easy-table": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.2.0.tgz", + "integrity": "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "optionalDependencies": { + "wcwidth": "^1.0.1" + } + }, + "node_modules/edge-paths": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", + "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/which": "^2.0.1", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/shirshak55" + } + }, + "node_modules/edgedriver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-6.3.0.tgz", + "integrity": "sha512-ggEQL+oEyIcM4nP2QC3AtCQ04o4kDNefRM3hja0odvlPSnsaxiruMxEZ93v3gDCKWYW6BXUr51PPradb+3nffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.18.0", + "@zip.js/zip.js": "^2.8.11", + "decamelize": "^6.0.1", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^5.3.3", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "which": "^6.0.0" + }, + "bin": { + "edgedriver": "bin/edgedriver.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/edgedriver/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/edgedriver/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "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-to-chromium": { + "version": "1.5.393", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz", + "integrity": "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==", + "dev": true, + "license": "ISC" + }, + "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/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/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/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": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/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", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-4.0.0.tgz", + "integrity": "sha512-Fqs7ChZm72y40wKjOFXBKg7nJZvQJmewP5/7LtePDdnah/+FH9Hp5sgMujSCMPXlxOAW2//1jrW9pnsY7o20vQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/expect-webdriverio": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/expect-webdriverio/-/expect-webdriverio-5.7.0.tgz", + "integrity": "sha512-jLOTrJoPBC3Wtd83ryHMbRcEajMGtAnn6OWtSnoJoDLt16nHvxVdjcI4Bc0n+1KAOr8DvQtlJ55f0M48kJtEpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/snapshot": "^4.1.7", + "deep-eql": "^5.0.2", + "expect": "^30.4.1", + "jest-matcher-utils": "^30.4.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@wdio/globals": "^9.0.0", + "@wdio/logger": "^9.0.0", + "webdriverio": "^9.0.0" + }, + "peerDependenciesMeta": { + "@wdio/globals": { + "optional": false + }, + "@wdio/logger": { + "optional": false + }, + "webdriverio": { + "optional": false + } + } + }, + "node_modules/expect-webdriverio/node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/expect-webdriverio/node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/expect-webdriverio/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/expect-webdriverio/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/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/extract-zip/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/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", + "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz", + "integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz", + "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^3.0.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.1", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "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/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/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/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/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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/geckodriver": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-6.1.0.tgz", + "integrity": "sha512-ZRXLa4ZaYTTgUO4Eefw+RsQCleugU2QLb1ME7qTYxxuRj51yAhfnXaItXNs5/vUzfIaDHuZ+YnSF005hfp07nQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.18.0", + "@zip.js/zip.js": "^2.8.11", + "decamelize": "^6.0.1", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "modern-tar": "^0.7.2" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": ">=20.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-port": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", + "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "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/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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/happy-dom": { + "version": "20.10.6", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.10.6.tgz", + "integrity": "sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/hosted-git-info/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/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/htmlfy": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.8.1.tgz", + "integrity": "sha512-xWROBw9+MEGwxpotll0h672KCaLrKKiCYzsyN8ZgL9cQbVumFnyvsk2JqiB9ELAV1GLj1GG/jxZUjV9OZZi/yQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "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/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/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/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/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/inquirer": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz", + "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/prompts": "^7.10.1", + "@inquirer/type": "^3.0.10", + "mute-stream": "^2.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "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-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-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-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "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-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unsafe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "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" + }, + "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/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/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/jest-diff/node_modules/chalk/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/jest-diff/node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/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/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/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/jest-matcher-utils/node_modules/chalk/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/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/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/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/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/jest-message-util/node_modules/chalk/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/jest-message-util/node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/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/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/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/jest-util/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/jest-util/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/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/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", + "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/jszip/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" + }, + "node_modules/jszip/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", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/katex": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.17.0.tgz", + "integrity": "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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", + "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", + "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" + }, + "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", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, + "node_modules/locate-app": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz", + "integrity": "sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/locate-app/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@promptbook/utils": "0.69.5", + "type-fest": "4.26.0", + "userhome": "1.0.1" + } + }, + "node_modules/locate-app/node_modules/type-fest": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.pickby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", + "integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==", + "dev": true, + "license": "MIT" + }, + "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" + }, + "node_modules/lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "dev": true, + "license": "MIT" + }, + "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/log-symbols/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/log-symbols/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/log-symbols/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/log-symbols/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/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lottie-web": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.13.0.tgz", + "integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==", + "license": "MIT" + }, + "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": "0.469.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.469.0.tgz", + "integrity": "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/micromark-extension-math/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/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/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/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/mocha/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/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/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/mocha/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/mocha/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/mocha/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mocha/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/mocha/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/mocha/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/mocha/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/mocha/node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/modern-tar": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", + "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "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/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "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/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-package-data": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-7.0.1.tgz", + "integrity": "sha512-linxNAT6M0ebEYZOx2tO6vBEFsVgnPpv+AVjk0wJHfaUIbq31Jm3T6vvZaarnOeWDh8ShnwXuaAyM7WT3RzErA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^8.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "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/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "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-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "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/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.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==", + "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-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/polished": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "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" + }, + "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/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/prosemirror-changeset": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", + "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz", + "integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.11", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz", + "integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.1.tgz", + "integrity": "sha512-rRqzZnRgkyh69XoOMrfFJHwauHscLBmHbq772kwbic1ymQAM8gXjzEbJse5j1ep2UO2HRIAQL0bY3kZ/RoqjVw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.8", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "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/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/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-draggable": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.7.0.tgz", + "integrity": "sha512-kTpANmKWVnFXiZ76Ag2ZowiFStuBYnJ606PI1TbUsOg29/400/JNIxI9+CuenhiAqFuXWJffz6F4UI3R51kUug==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-is": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "license": "MIT" + }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-resizable": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.2.0.tgz", + "integrity": "sha512-3NKQ0SLZV7rs3LQHeXlOzDSRQfFrkX6TVet77/Qk03zqiZyee37b7N8/gwDJAA8UUjRz7PdWCCy49hcso45SMQ==", + "license": "MIT", + "dependencies": { + "prop-types": "15.x", + "react-draggable": "^4.5.0" + }, + "peerDependencies": { + "react": ">= 16.3", + "react-dom": ">= 16.3" + } + }, + "node_modules/react-window": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz", + "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/read-pkg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", + "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^6.0.0", + "parse-json": "^7.0.0", + "type-fest": "^4.2.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.1.0.tgz", + "integrity": "sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0", + "read-pkg": "^8.1.0", + "type-fest": "^4.2.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/read-pkg-up/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/read-pkg/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-pkg/node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/read-pkg/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/read-pkg/node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", + "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.21.4", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^3.0.0", + "lines-and-columns": "^2.0.3", + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "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", + "dependencies": { + "minimatch": "^5.1.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", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/recursive-readdir/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/recursive-readdir/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/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/rehype-katex/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/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/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", + "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rgb2hex": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", + "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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/safaridriver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.1.tgz", + "integrity": "sha512-jkg4434cYgtrIF2AeY/X0Wmd2W73cK5qIEFE3hDrrQenJH/2SDJIXGvPAigfvQTcE9+H31zkiNHbUqcihEiMRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.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/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "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/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/scroll-into-view-if-needed": { + "version": "2.2.31", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", + "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^1.0.20" + } + }, + "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/serialize-error": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-12.0.0.tgz", + "integrity": "sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.31.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "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/smol-toml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", + "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spacetrim": { + "version": "0.11.59", + "resolved": "https://registry.npmjs.org/spacetrim/-/spacetrim-0.11.59.tgz", + "integrity": "sha512-lLYsktklSRKprreOm7NXReW8YiX2VBjbgmXYEziOoGf/qsJqAEACaDvoTtUOycwjpaSh+bT8eu0KrJn7UNxiCg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/spacetrim/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "license": "Apache-2.0" + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stream-buffers": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.3.tgz", + "integrity": "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "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": "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/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/string-width-cjs/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/string-width-cjs/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/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "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/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/strip-ansi/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/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.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/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "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/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "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/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/use-merge-value": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-merge-value/-/use-merge-value-1.2.0.tgz", + "integrity": "sha512-DXgG0kkgJN45TcyoXL49vJnn55LehnrmoHc7MbKi+QDBvr8dsesqws8UlyIWGHMR+JXgxc1nvY+jDGMlycsUcw==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.x" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/userhome": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.1.tgz", + "integrity": "sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/wait-port": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "commander": "^9.3.0", + "debug": "^4.3.4" + }, + "bin": { + "wait-port": "bin/wait-port.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/wait-port/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/wait-port/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/wait-port/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/wait-port/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/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", + "optional": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webdriver": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.29.1.tgz", + "integrity": "sha512-uhxYap3qQXC9H2V8SDr7vcy0blZETeri4goLwbw1TFq4EZHF1Dv503Zc0PAjfkNQJCD4/rzAyr07+EX72kx1pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.29.1", + "@wdio/logger": "9.29.1", + "@wdio/protocols": "9.29.1", + "@wdio/types": "9.29.1", + "@wdio/utils": "9.29.1", + "deepmerge-ts": "^7.0.3", + "https-proxy-agent": "^7.0.6", + "undici": "^6.21.3", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriverio": { + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.29.1.tgz", + "integrity": "sha512-UIplAnvbjdE0tucHVR/8Uk0Y7rz72VaEx/s0Tq91fMdXm+m+Za16e+b5tObh4xEEfyCITODPfzgBva9rA+xApQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.29.1", + "@wdio/logger": "9.29.1", + "@wdio/protocols": "9.29.1", + "@wdio/repl": "9.16.2", + "@wdio/types": "9.29.1", + "@wdio/utils": "9.29.1", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.8.1", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^12.0.0", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.29.1" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": ">=22.x || <=24.x" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/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/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/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/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.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/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "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": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/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/yauzl/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/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/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "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 + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/frontends/desktop/package.json b/frontends/desktop/package.json new file mode 100644 index 000000000..2f0694031 --- /dev/null +++ b/frontends/desktop/package.json @@ -0,0 +1,68 @@ +{ + "name": "genericagent-desktop", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "vitest run", + "test:bridge": "vitest run src/__tests__/bridge/", + "test:stress": "vitest run src/__tests__/stress/", + "test:bundle": "node scripts/assert-dist-built.mjs", + "test:packaging": "node scripts/test-packaging.mjs", + "test:e2e-isolation": "npm run build && node scripts/assert-production-e2e-isolation.mjs", + "test:e2e-types": "tsc -p tsconfig.e2e.json", + "test:all": "vitest run && node scripts/assert-dist-built.mjs", + "e2e:browser": "tsx e2e/run.ts --mode=browser", + "e2e:desktop": "tsx e2e/run.ts --mode=desktop", + "e2e:desktop:full": "tsx e2e/run.ts --mode=desktop --suite=full", + "e2e:canary": "tsx e2e/canary.ts", + "e2e:doctor": "tsx e2e/doctor.ts", + "tauri": "tauri", + "build:dmg": "tauri build --bundles dmg && bash scripts/post-dmg.sh src-tauri/target/release/bundle/dmg/*.dmg", + "build:app": "tauri build --bundles app", + "postbuild:dmg": "bash scripts/post-dmg.sh src-tauri/target/release/bundle/dmg/*.dmg" + }, + "dependencies": { + "@douyinfe/semi-icons": "^2.71.1", + "@douyinfe/semi-ui": "^2.71.1", + "@lobehub/icons": "^5.10.1", + "@types/prismjs": "^1.26.6", + "@vscode/codicons": "^0.0.45", + "katex": "^0.17.0", + "prismjs": "^1.30.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "rehype-katex": "^7.0.1", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "@wdio/cli": "^9.29.1", + "@wdio/local-runner": "^9.29.1", + "@wdio/mocha-framework": "^9.29.1", + "@wdio/spec-reporter": "^9.29.1", + "@wdio/tauri-plugin": "^1.2.0", + "@wdio/tauri-service": "^1.2.0", + "happy-dom": "^20.10.6", + "tsx": "^4.23.1", + "typescript": "^5.6.3", + "vite": "^6.0.5", + "vitest": "^4.1.9", + "webdriverio": "^9.29.1" + }, + "overrides": { + "@wdio/native-utils": "2.5.0" + } +} diff --git a/frontends/desktop/packaging/CHECKLIST.md b/frontends/desktop/packaging/CHECKLIST.md new file mode 100644 index 000000000..c85bb6ab6 --- /dev/null +++ b/frontends/desktop/packaging/CHECKLIST.md @@ -0,0 +1,48 @@ +# GenericAgent Desktop 测试 CHECKLIST + +--- + +## 一、聊天页面 + +- [ ] 发消息能正常收到回复,顶栏状态在「运行中」「就绪」之间切换,中文输入法 Enter 不会误发,超长粘贴会被截断 +- [ ] 上传文件、拖拽图片到输入框,能发送且 agent 能识别,流式回复中可中断,悬停消息能复制全文 +- [ ] 代码块/数学公式/多轮折叠都正常,长回复(2000 字以上)末尾不丢字 +- [ ] 起始页预设卡片可跳转(指挥家)或触发对应模式(Plan / Goal 等),自定义预设可增删且刷新后仍在 +- [ ] 切换模型后用量页能记录新模型,设置页调字号后聊天字体实时变,ask_user 弹问题卡能正常答复 +- [ ] 切换到一个 300 轮对话以上的 session,确保页面不卡顿 + +--- + +## 二、会话列表 + +- [ ] 新对话能创建,空态文案正常显示,搜索框能按标题过滤 +- [ ] 置顶/取消置顶/重命名/删除(激活和非激活分别测),都即时生效且刷新后持久 +- [ ] 右栏可拖窄拖宽和整体折叠,跑任务的会话有运行中视觉标识 + +--- + +## 三、指挥家 + +- [ ] 派一个多步骤目标,顶栏状态进入运行中,右侧分工进度出现子任务卡片,任务跑完主区有简报回复 + +--- + +## 四、后台服务 + +- [ ] 确保修改的配置能保存到本地,除此之外都是原有功能,不需要测试 + +--- + +## 五、用量 + +- [ ] 在聊天和指挥家页面发送消息并收到回复,确保聊天和指挥家都能被统计用量,且模型无误 +- [ ] 表格列宽固定不跳动,会话名过长用省略号显示且悬停有 tooltip,点击会话行可展开按时间倒序的明细 +- [ ] 趋势图折叠展开后不变形,日期范围筛选 + 重置生效,已删除的会话标「此会话已删除」 + +--- + +## 六、设置页面(外观 / 字号 / 语言 / 模型管理) + +- [ ] 外观浅色/深色切换,浅色下「素色」开关可用,聊天字号 10-20 可调,关弹窗再开后所有设置仍生效 +- [ ] 中英文切换后整个 UI 文案跟着变,无中英混杂 +- [ ] 模型可添加/编辑/删除,添加弹窗里的 DeepSeek / 通义千问 快速接入会预填地址+协议+模型,用户只需粘贴 API key 就能保存使用 diff --git a/frontends/desktop/packaging/README.md b/frontends/desktop/packaging/README.md new file mode 100644 index 000000000..20376f470 --- /dev/null +++ b/frontends/desktop/packaging/README.md @@ -0,0 +1,57 @@ +# packaging + +桌面端发布相关材料。本目录的内容**不会**整体打进发布包——CI +(`.github/workflows/desktop-release-package.yml`)只从这里挑选 `scripts/` 下的 +安装/卸载脚本拷进各平台的发布产物,其余文件对构建打包过程是只读参考。 + +## 目录结构 + +``` +frontends/desktop/packaging/ +├── README.md # 本说明 +├── CHECKLIST.md # 发布前功能测试清单(测试协调用,不参与打包) +├── TODO.md # 各平台测试分工与计划(测试协调用,不参与打包) +└── scripts/ # ← 唯一被 CI 消费的内容 + ├── windows/ + │ ├── install_windows.ps1 # 环境准备脚本 + │ ├── uninstall.bat # 卸载入口(向用户确认后调用 ps1) + │ └── uninstall_windows.ps1 + ├── linux/ + │ ├── install_linux.sh + │ └── uninstall.sh + └── macos/ + ├── install_macos.sh + └── uninstall.command +``` + +## CI 如何使用这些脚本 + +`desktop-release-package.yml` 在打各平台 portable 包时,把对应平台的脚本 +`cp` 进发布目录(例如 Windows 包里放 `install_windows.ps1` / +`uninstall.bat` / `uninstall_windows.ps1`)。**修改安装/卸载行为只需改 +`scripts/` 下的文件,不需要动 workflow。** + +> 说明:实际的桌面壳二进制(`GenericAgent.exe` / `.AppImage` / `.app`)由 CI +> 构建生成并发布到 GitHub Release,不在本仓库内提交,也不在本目录占位。 + +## 自动化测试体系 + +打包前通过 `npm run test:all` 一键验证。测试分四层: + +| 层 | 命令 | 说明 | +|---|---|---| +| Layer 1 | `npm run test` | UI/store/逻辑测试(vitest + happy-dom) | +| Layer 2 | `npm run test:bridge` | Bridge 连接协议、状态机、API 契约 | +| Layer 3 | `npm run test:bundle` | 构建产物完整性(dist 结构、JS bundle 存在性) | +| Layer 4 | `npm run test:packaging` | 打包前置条件(tauri.conf、icons、脚本语法) | + +分层跑: + +```bash +npm run test # Layer 1 全部 +npm run test:stress # Layer 1 压力子集 +npm run test:bridge # Layer 2 +npm run test:bundle # Layer 3(需先 npm run build) +npm run test:packaging # Layer 4 +npm run test:all # Layer 1-3 一键 +``` diff --git a/frontends/desktop/packaging/TODO.md b/frontends/desktop/packaging/TODO.md new file mode 100644 index 000000000..85cd6858d --- /dev/null +++ b/frontends/desktop/packaging/TODO.md @@ -0,0 +1,85 @@ +# GenericAgent Desktop 测试 TODO + +## 目标 + +目前打算采用 git action 构建发布版,本轮测试主要关注三个结果: + +1. 测试者优先验证已有 Release 产物是否能在目标系统运行。 +2. 若 Release 产物不可用,再记录失败原因,并考虑本地构建或调整 GitHub Action 构建方法。 +3. Release 产物能启动后,再做核心功能兼容性测试。 + +--- + +## 系统覆盖 + +重点测试 Windows 和 Linux; + +| 优先级 | 系统 | 架构/版本 | 测试员 | 状态 | 备注 | +|---|---|---|---|---|---| +| P0 | Windows 11 | x64 | 无 | 开发过程中已测试 | 当前最优先 Windows 环境 | +| P0 | Windows 10 | x64 | 杨航 | TODO | 常见 Windows 环境 | +| P0 | Ubuntu 24.04 LTS | x64 | 张景铭 | TODO | 当前常见 Linux LTS | +| P0 | Ubuntu 22.04 LTS | x64 | 张景铭 | TODO | 仍然常见的 Linux LTS | +| P1 | Debian 12 | x64 | 曹兮 | TODO | 可选,覆盖 Debian 系 | +| Owner | macOS | Apple Silicon 或 Intel | 杨航 | TODO | | + + +--- + +## 每个系统需要完成的任务 + +### 任务 1:下载并验证已有 Release 产物 + +测试员优先使用已有 Release,不要求先在本机生成程序壳。 + +当前测试 Release: + +```text +https://github.com/dd3xp/GenericAgent_Desktop/releases/tag/desktop-windows-test-3-1 +``` + +当前资产命名: + +```text +GenericAgent-Desktop-Windows.exe +GenericAgent-Desktop-Linux.AppImage +SHA256SUMS.txt +``` + +测试步骤: + +1. 下载对应系统的 Release 产物和 `SHA256SUMS.txt`; +2. 启动环境配置脚本 +3. 运行程序壳; +4. 记录是否能打开主界面、是否能连上本地 bridge/后端; + +环境配置脚本 Windows 可参考: + +```powershell +# 在仓库根目录执行 +.\frontends\desktop\packaging\scripts\windows\install_windows.ps1 +``` + +环境配置脚本 Linux 可参考: + +```bash +chmod +x frontends/desktop/packaging/scripts/linux/install_linux.sh +./frontends/desktop/packaging/scripts/linux/install_linux.sh --mode PrepareOnly +chmod +x GenericAgent-Desktop-Linux.AppImage +./GenericAgent-Desktop-Linux.AppImage +``` + +说明:Linux 脚本 `frontends/desktop/packaging/scripts/linux/install_linux.sh` 目前作为参考实现使用,已在 Ubuntu 24.04.4 LTS x64 上做过初步试用,但仍需按清单做完整验证。脚本核心做法是准备运行环境,并写入用户级桌面配置(如 `~/.ga_desktop_settings.json`),让程序壳能找到 `project_dir`、`python_path` 和 bridge 脚本。 + +### 任务 2:Release 不可用时的升级路径 + +如果 Release 产物在目标系统不可用: +1. 如果只是目标机缺少运行环境,优先修正环境配置脚本; +2. 如果 Release 产物本身不兼容,尝试在目标系统本地构建; +3. 如果本地构建可行,再考虑修改 GitHub Action 的 Linux/Windows 构建方法,让 CI 产物与本地成功产物保持一致。 + +### 任务 3:核心功能兼容性测试 + +Release 产物可以启动并连上后端后,再根据 `frontends/desktop/packaging/CHECKLIST.md` 测试不同平台下的界面和核心功能兼容性。 + +--- diff --git a/frontends/desktop/packaging/scripts/linux/install_linux.sh b/frontends/desktop/packaging/scripts/linux/install_linux.sh new file mode 100755 index 000000000..f8b14242c --- /dev/null +++ b/frontends/desktop/packaging/scripts/linux/install_linux.sh @@ -0,0 +1,429 @@ +#!/usr/bin/env bash +set -euo pipefail + +# The desktop AppImage injects PYTHONHOME/PYTHONPATH/LD_LIBRARY_PATH pointing at its own +# read-only mount, which breaks the bundled python ("No module named 'encodings'"). Always +# run with a clean python env so the embedded interpreter uses its own stdlib/libs. +unset PYTHONHOME PYTHONPATH LD_LIBRARY_PATH + +# GenericAgent Desktop Linux installer. +# Expected normal location: GenericAgent/frontends/install_linux.sh +# Expected AppImage: GenericAgent/frontends/GenericAgent.AppImage +# +# Usage: +# ./install_linux.sh +# ./install_linux.sh --project-dir /path/to/GenericAgent +# ./install_linux.sh --mode PrepareOnly|Launch|BridgeOnly +# ./install_linux.sh --skip-pip-install +# +# What this script does: +# 1. Resolve the GenericAgent project root containing agentmain.py. +# 2. Resolve/create a Python runtime under project .venv unless --no-venv is used. +# 3. Install minimal desktop bridge dependencies unless --skip-pip-install is used. +# 4. Write ~/.ga_desktop_settings.json for the AppImage desktop shell. +# 5. chmod +x GenericAgent.AppImage. +# 6. Create/update desktop and application-menu .desktop launchers. +# 7. By default, do not launch the app; use --mode Launch if desired. + +PROJECT_DIR="" +PYTHON_PATH="" +MODE="PrepareOnly" +NO_VENV=0 +SKIP_PIP_INSTALL=0 +APPIMAGE_PATH="" +WHEEL_DIR="" +EXTRA_PACKAGES="" + +log_step() { printf '\n==> %s\n' "$*" >&2; } +log_ok() { printf '[OK] %s\n' "$*" >&2; } +log_warn() { printf '[WARN] %s\n' "$*" >&2; } +fail() { printf '[ERROR] %s\n' "$*" >&2; exit 1; } + +usage() { + sed -n '1,28p' "$0" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --project-dir|-p) + [[ $# -ge 2 ]] || fail "Missing value for $1" + PROJECT_DIR="$2"; shift 2 ;; + --python-path) + [[ $# -ge 2 ]] || fail "Missing value for $1" + PYTHON_PATH="$2"; shift 2 ;; + --mode|-m) + [[ $# -ge 2 ]] || fail "Missing value for $1" + MODE="$2"; shift 2 ;; + --appimage) + [[ $# -ge 2 ]] || fail "Missing value for $1" + APPIMAGE_PATH="$2"; shift 2 ;; + --no-venv) + NO_VENV=1; shift ;; + --skip-pip-install) + SKIP_PIP_INSTALL=1; shift ;; + --wheel-dir) + [[ $# -ge 2 ]] || fail "Missing value for $1" + WHEEL_DIR="$2"; shift 2 ;; + --extra-packages) + [[ $# -ge 2 ]] || fail "Missing value for $1" + EXTRA_PACKAGES="$2"; shift 2 ;; + --help|-h) + usage; exit 0 ;; + *) + fail "Unknown argument: $1" ;; + esac +done + +case "$MODE" in + PrepareOnly|Launch|BridgeOnly) ;; + *) fail "Unsupported --mode '$MODE'. Expected PrepareOnly, Launch, or BridgeOnly." ;; +esac + +resolve_script_root() { + local src="${BASH_SOURCE[0]}" + while [[ -L "$src" ]]; do + local dir + dir="$(cd -P "$(dirname "$src")" && pwd)" + src="$(readlink "$src")" + [[ "$src" != /* ]] && src="$dir/$src" + done + cd -P "$(dirname "$src")" && pwd +} + +abs_path() { + python3 - "$1" <<'PY' +import os, sys +print(os.path.abspath(os.path.expanduser(sys.argv[1]))) +PY +} + +has_agentmain() { [[ -f "$1/agentmain.py" ]]; } + +find_project_root() { + local script_root="$1" + if [[ -n "$PROJECT_DIR" ]]; then + local p + p="$(abs_path "$PROJECT_DIR")" + has_agentmain "$p" || fail "Project dir does not contain agentmain.py: $PROJECT_DIR" + printf '%s\n' "$p" + return + fi + + local candidates=( + "$script_root/.." + "$PWD/.." + "$PWD" + "$script_root" + "$script_root/../.." + "$HOME/GenericAgent" + "$HOME/GenericAgent_Desktop" + "$HOME/GenericAgent_Desktop_test" + ) + + local c p + for c in "${candidates[@]}"; do + [[ -e "$c" ]] || continue + p="$(abs_path "$c")" + if has_agentmain "$p"; then + printf '%s\n' "$p" + return + fi + done + + fail "Cannot locate GenericAgent project root. Put this script in GenericAgent/frontends or pass --project-dir /path/to/GenericAgent." +} + +python_supported() { + local py="$1" + "$py" - <<'PY' >/dev/null 2>&1 +import sys +raise SystemExit(0 if (3, 10) <= sys.version_info[:2] < (3, 14) else 1) +PY +} + +python_exe() { + local py="$1" + "$py" - <<'PY' +import sys +print(sys.executable) +PY +} + +find_python() { + if [[ -n "$PYTHON_PATH" ]]; then + python_supported "$PYTHON_PATH" || fail "Specified Python is not supported: $PYTHON_PATH. Need Python >=3.10,<3.14." + python_exe "$PYTHON_PATH" + return + fi + + local py + for py in python3.12 python3.11 python3.10 python3 python; do + if command -v "$py" >/dev/null 2>&1 && python_supported "$py"; then + python_exe "$py" + return + fi + done + fail "No supported Python found. Install Python 3.10-3.13 or pass --python-path." +} + +ensure_venv() { + local root="$1" + local base_py="$2" + if [[ "$NO_VENV" -eq 1 ]]; then + printf '%s\n' "$base_py" + return + fi + + local venv="$root/.venv" + local vpy="$venv/bin/python" + if [[ ! -x "$vpy" ]]; then + # NOTE: ensure_venv's stdout is captured by the caller as the python path + # (PY="$(ensure_venv ...)"), so progress markers must NOT be printed here — + # they go on the main-flow stdout instead. Only the venv path goes to stdout. + log_step "Create virtual environment: $venv" + "$base_py" -m venv "$venv" || fail "Failed to create venv. On Debian/Ubuntu install python3-venv." + fi + python_supported "$vpy" || fail "Venv Python is not supported: $vpy" + printf '%s\n' "$vpy" +} + +install_dependencies() { + local root="$1" + local py="$2" + if [[ "$SKIP_PIP_INSTALL" -eq 1 ]]; then + log_warn "Skipped pip install because --skip-pip-install was used." + return + fi + + printf 'GAPROGRESS|deps\n' + if [[ -n "$WHEEL_DIR" ]]; then + # Offline (portable bundle): install from local wheels only, no network, no pip self-upgrade. + log_step "Install dependencies offline from $WHEEL_DIR" + # Install deps directly (NOT an editable -e of the source): an editable install bakes the + # project's absolute path into a .pth. Combined with --no-venv (deps go into the relocatable + # embedded python) this keeps the portable bundle movable. The bridge adds the source to + # sys.path itself (ensure_ga_import_path), so no install of the project is needed. + # shellcheck disable=SC2086 + "$py" -m pip install --no-index --find-links "$WHEEL_DIR" \ + "requests>=2.28" "beautifulsoup4>=4.12" "bottle>=0.12" "simple-websocket-server>=0.4" "aiohttp>=3.9" psutil $EXTRA_PACKAGES \ + || fail "Offline pip install failed (check wheel dir)." + else + log_step "Install/refresh minimal Python dependencies" + "$py" -m pip install --upgrade pip setuptools wheel || fail "pip bootstrap failed." + # shellcheck disable=SC2086 + "$py" -m pip install -e "$root" psutil $EXTRA_PACKAGES || fail "pip install failed." + fi + printf 'GAPROGRESS|done\n' +} + +ensure_mykey() { + local root="$1" + local mykey="$root/mykey.py" + local tpl="$root/mykey_template.py" + if [[ -f "$mykey" ]]; then + log_ok "mykey.py exists" + elif [[ -f "$tpl" ]]; then + cp "$tpl" "$mykey" + log_warn "Created mykey.py from mykey_template.py. Please fill API keys before model calls." + else + log_warn "mykey.py and mykey_template.py are missing. Model config may need manual setup." + fi +} + +write_desktop_settings() { + local root="$1" + local py="$2" + local bridge="$root/frontends/desktop_bridge.py" + local settings_path="$HOME/.ga_desktop_settings.json" + [[ -f "$bridge" ]] || fail "desktop_bridge.py not found: $bridge" + + "$py" - "$settings_path" "$py" "$root" "$bridge" <<'PY' +import json, pathlib, sys +settings_path, python_path, project_dir, bridge_script = sys.argv[1:5] +obj = { + "python_path": python_path, + "project_dir": project_dir, + "bridge_script": bridge_script, +} +pathlib.Path(settings_path).write_text(json.dumps(obj, indent=2), encoding="utf-8") +PY + log_ok "Wrote desktop settings: $settings_path" +} + +find_appimage() { + local root="$1" + local script_root="$2" + + if [[ -n "$APPIMAGE_PATH" ]]; then + local p + p="$(abs_path "$APPIMAGE_PATH")" + [[ -f "$p" ]] || fail "AppImage not found: $APPIMAGE_PATH" + printf '%s\n' "$p" + return + fi + + local candidates=( + "$script_root/GenericAgent.AppImage" + "$root/frontends/GenericAgent.AppImage" + ) + local p + for p in "${candidates[@]}"; do + if [[ -f "$p" ]]; then + printf '%s\n' "$(abs_path "$p")" + return + fi + done + + # Last-resort support for older versioned artifacts, but generated releases should use GenericAgent.AppImage. + find "$root/frontends" -maxdepth 1 -type f -name '*.AppImage' 2>/dev/null | sort | head -n 1 || true +} + +find_icon() { + local root="$1" + local candidates=( + "$root/frontends/desktop/src-tauri/icons/icon.png" + "$root/frontends/desktop/src-tauri/icons/128x128.png" + "$root/frontends/desktop/src-tauri/icons/128x128@2x.png" + ) + local p + for p in "${candidates[@]}"; do + if [[ -f "$p" ]]; then + printf '%s\n' "$p" + return + fi + done + printf '%s\n' "generic" +} + +shell_quote() { + python3 - "$1" <<'PY' +import shlex, sys +print(shlex.quote(sys.argv[1])) +PY +} + +write_desktop_file() { + local out="$1" + local root="$2" + local appimage="$3" + local icon="$4" + mkdir -p "$(dirname "$out")" + cat > "$out" </dev/null 2>&1; then + gio set "$desktop_file" metadata::trusted true >/dev/null 2>&1 || true + gio set "$menu_file" metadata::trusted true >/dev/null 2>&1 || true + fi + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database "$menu_dir" >/dev/null 2>&1 || true + fi + + log_ok "Desktop launcher: $desktop_file" + log_ok "If your file manager marks the desktop icon as untrusted, right-click it and choose Allow Launching." +} + +start_bridge() { + local root="$1" + local py="$2" + local bridge="$root/frontends/desktop_bridge.py" + [[ -f "$bridge" ]] || fail "desktop_bridge.py not found: $bridge" + log_step "Start bridge: $bridge" + export PYTHONPATH="$root:$root/frontends:${PYTHONPATH:-}" + exec "$py" "$bridge" +} + +launch_appimage() { + local appimage="$1" + log_step "Launch AppImage" + nohup "$appimage" >/tmp/genericagent-desktop-appimage.log 2>&1 & + log_ok "Started: $appimage" + log_ok "Log: /tmp/genericagent-desktop-appimage.log" +} + +SCRIPT_ROOT="$(resolve_script_root)" + +log_step "Resolve project root" +ROOT="$(find_project_root "$SCRIPT_ROOT")" +log_ok "Project root: $ROOT" + +# Portable self-prepare (offline, --wheel-dir set): the AppImage is already running and +# manages itself, so skip AppImage discovery and desktop-launcher creation. +PORTABLE_PREPARE=0 +[[ -n "$WHEEL_DIR" ]] && PORTABLE_PREPARE=1 + +APPIMAGE="" +if [[ "$PORTABLE_PREPARE" -eq 0 ]]; then + log_step "Resolve AppImage" + APPIMAGE="$(find_appimage "$ROOT" "$SCRIPT_ROOT")" + [[ -n "$APPIMAGE" ]] || fail "GenericAgent.AppImage not found. Put it next to install_linux.sh in GenericAgent/frontends/." + chmod +x "$APPIMAGE" + log_ok "AppImage: $APPIMAGE" +fi + +log_step "Resolve Python" +BASE_PY="$(find_python)" +log_ok "Base Python: $BASE_PY" + +# Progress marker on the real stdout (the Rust shell reads it); must be outside the +# command-substitution that captures ensure_venv's stdout as the python path. +printf 'GAPROGRESS|venv\n' +PY="$(ensure_venv "$ROOT" "$BASE_PY")" +log_ok "Runtime Python: $PY" + +install_dependencies "$ROOT" "$PY" +ensure_mykey "$ROOT" +write_desktop_settings "$ROOT" "$PY" +if [[ "$PORTABLE_PREPARE" -eq 0 ]]; then + install_desktop_launchers "$ROOT" "$APPIMAGE" +fi + +case "$MODE" in + PrepareOnly) + log_ok "Linux desktop setup finished. Start GenericAgent Desktop from the desktop icon or application menu." + ;; + Launch) + launch_appimage "$APPIMAGE" + ;; + BridgeOnly) + start_bridge "$ROOT" "$PY" + ;; +esac diff --git a/frontends/desktop/packaging/scripts/linux/uninstall.sh b/frontends/desktop/packaging/scripts/linux/uninstall.sh new file mode 100644 index 000000000..ed0c083c3 --- /dev/null +++ b/frontends/desktop/packaging/scripts/linux/uninstall.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# GenericAgent Desktop — portable uninstall (Linux). +# +# Removes everything THIS portable bundle put on the machine, then deletes the +# bundle folder itself: +# 1. Stop the bundle's processes (GUI + bridge/conductor/scheduler python) — +# only processes whose exe/cmdline live inside this bundle, so a second +# install on the same machine is left alone. +# 2. Remove the desktop shortcut (GenericAgent.desktop) — only when its Exec +# points into this bundle. +# 3. Remove ~/.ga_desktop_settings.json (shared settings; other bundles rebuild +# it on next launch). +# 4. Remove the WebKitGTK data dir (~/.local/share + ~/.cache for the app id). +# 5. Delete the bundle folder. +set -u + +BUNDLE="$(cd "$(dirname "$0")" && pwd)" +APP_ID="com.genericagent.app" + +echo "============================================================" +echo " GenericAgent Desktop - Uninstall" +echo "============================================================" +echo +echo "This will completely remove GenericAgent from this computer:" +echo " - stop its background services (bridge 14168 / conductor 8900)" +echo " - delete the desktop shortcut" +echo " - delete settings (~/.ga_desktop_settings.json)" +echo " - delete WebView data (~/.local/share/$APP_ID, ~/.cache/$APP_ID)" +echo " - delete THIS folder and everything in it:" +echo " $BUNDLE" +echo +echo "This cannot be undone." +echo +read -r -p "Type Y to uninstall, anything else to cancel: " CONFIRM +case "$CONFIRM" in + y|Y) ;; + *) echo; echo "Cancelled. Nothing was changed."; exit 0 ;; +esac + +echo +echo "==> Stopping GenericAgent backend services" +# Best-effort graceful exit first. +curl -fsS -m 3 -X POST "http://127.0.0.1:14168/services/bridge/exit" >/dev/null 2>&1 || true +sleep 1 + +# Kill any process whose exe or command line lives inside this bundle — never touch +# a second install (different path). Skip our own uninstall shell and its parent. +for pid in $(ls /proc 2>/dev/null | grep -E '^[0-9]+$'); do + [ "$pid" = "$$" ] && continue + [ "$pid" = "$PPID" ] && continue + exe="$(readlink -f "/proc/$pid/exe" 2>/dev/null || true)" + cl="$( (cat "/proc/$pid/cmdline" 2>/dev/null || true) | tr '\0' ' ')" + case "$exe $cl" in + *"$BUNDLE"*) kill -9 "$pid" 2>/dev/null && echo " killed PID $pid" ;; + esac +done +echo "[OK] backend stopped" + +echo "==> Removing desktop shortcut" +removed_shortcut=0 +for f in "$HOME/.local/share/applications/GenericAgent.desktop" \ + "${XDG_DESKTOP_DIR:-$HOME/Desktop}/GenericAgent.desktop" \ + "$HOME/桌面/GenericAgent.desktop"; do + [ -f "$f" ] || continue + if grep -qF "$BUNDLE" "$f" 2>/dev/null; then + rm -f "$f" && { echo "[OK] removed $f"; removed_shortcut=1; } + else + echo " $f points to another bundle; left in place" + fi +done +[ "$removed_shortcut" = 0 ] && echo " no desktop shortcut for this bundle found" + +echo "==> Removing settings file" +if [ -f "$HOME/.ga_desktop_settings.json" ]; then + rm -f "$HOME/.ga_desktop_settings.json" && echo "[OK] removed ~/.ga_desktop_settings.json" +else + echo " no settings file found" +fi + +echo "==> Removing WebView data" +for d in "$HOME/.local/share/$APP_ID" "$HOME/.cache/$APP_ID"; do + if [ -d "$d" ]; then rm -rf "$d" && echo "[OK] removed $d"; fi +done + +echo "==> Removing the bundle folder" +cd /tmp || cd / +if rm -rf "$BUNDLE" 2>/dev/null && [ ! -e "$BUNDLE" ]; then + echo "[OK] removed $BUNDLE" +else + # A still-mounted AppImage or busy file can block removal; retry detached after we exit. + nohup bash -c 'for i in $(seq 1 20); do rm -rf "'"$BUNDLE"'" 2>/dev/null; [ -e "'"$BUNDLE"'" ] || exit 0; sleep 1; done' >/dev/null 2>&1 & + echo "[OK] bundle folder will be removed after exit: $BUNDLE" +fi + +echo +echo "GenericAgent has been uninstalled." diff --git a/frontends/desktop/packaging/scripts/macos/install_macos.sh b/frontends/desktop/packaging/scripts/macos/install_macos.sh new file mode 100755 index 000000000..a03b74da7 --- /dev/null +++ b/frontends/desktop/packaging/scripts/macos/install_macos.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +set -euo pipefail + +# GenericAgent Desktop macOS portable installer/preparer. +# Intended bundle layout: +# GenericAgent-Desktop-macOS/ +# GenericAgent.app +# runtime/ +# python/ wheels/ install_macos.sh app/ +# +# Usage: +# ./install_macos.sh --python-path /path/to/python3 --project-dir /path/to/runtime/app --wheel-dir /path/to/wheels --mode PrepareOnly +# ./install_macos.sh --mode BridgeOnly + +PROJECT_DIR="" +PYTHON_PATH="" +MODE="PrepareOnly" +NO_VENV=0 +SKIP_PIP_INSTALL=0 +WHEEL_DIR="" +EXTRA_PACKAGES="" +APP_PATH="" + +log_step() { printf '\n==> %s\n' "$*" >&2; } +log_ok() { printf '[OK] %s\n' "$*" >&2; } +log_warn() { printf '[WARN] %s\n' "$*" >&2; } +fail() { printf '[ERROR] %s\n' "$*" >&2; exit 1; } +progress() { printf 'GAPROGRESS|%s\n' "$1"; } + +usage() { sed -n '1,18p' "$0"; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --project-dir|-p) [[ $# -ge 2 ]] || fail "Missing value for $1"; PROJECT_DIR="$2"; shift 2 ;; + --python-path) [[ $# -ge 2 ]] || fail "Missing value for $1"; PYTHON_PATH="$2"; shift 2 ;; + --mode|-m) [[ $# -ge 2 ]] || fail "Missing value for $1"; MODE="$2"; shift 2 ;; + --app|-a) [[ $# -ge 2 ]] || fail "Missing value for $1"; APP_PATH="$2"; shift 2 ;; + --no-venv) NO_VENV=1; shift ;; + --skip-pip-install) SKIP_PIP_INSTALL=1; shift ;; + --wheel-dir) [[ $# -ge 2 ]] || fail "Missing value for $1"; WHEEL_DIR="$2"; shift 2 ;; + --extra-packages) [[ $# -ge 2 ]] || fail "Missing value for $1"; EXTRA_PACKAGES="$2"; shift 2 ;; + --help|-h) usage; exit 0 ;; + *) fail "Unknown argument: $1" ;; + esac +done + +case "$MODE" in + PrepareOnly|Launch|BridgeOnly) ;; + *) fail "Unsupported --mode '$MODE'. Expected PrepareOnly, Launch, or BridgeOnly." ;; +esac + +resolve_script_root() { + local src="${BASH_SOURCE[0]}" + while [[ -L "$src" ]]; do + local dir + dir="$(cd -P "$(dirname "$src")" && pwd)" + src="$(readlink "$src")" + [[ "$src" != /* ]] && src="$dir/$src" + done + cd -P "$(dirname "$src")" && pwd +} + +abs_path() { + "$PYTHON_FOR_ABS" - "$1" <<'PY' +import os, sys +print(os.path.abspath(os.path.expanduser(sys.argv[1]))) +PY +} + +SCRIPT_ROOT="$(resolve_script_root)" +PYTHON_FOR_ABS="${PYTHON_PATH:-python3}" + +if [[ -z "$PROJECT_DIR" ]]; then + for c in "$SCRIPT_ROOT/app" "$SCRIPT_ROOT/../runtime/app" "$SCRIPT_ROOT/.." "$PWD"; do + if [[ -f "$c/agentmain.py" ]]; then PROJECT_DIR="$c"; break; fi + done +fi +[[ -n "$PROJECT_DIR" ]] || fail "Cannot resolve project dir; pass --project-dir" +PROJECT_DIR="$(abs_path "$PROJECT_DIR")" +[[ -f "$PROJECT_DIR/agentmain.py" ]] || fail "Project dir does not contain agentmain.py: $PROJECT_DIR" + +if [[ -z "$PYTHON_PATH" ]]; then + if [[ -x "$SCRIPT_ROOT/python/bin/python3" ]]; then PYTHON_PATH="$SCRIPT_ROOT/python/bin/python3"; else PYTHON_PATH="python3"; fi +fi +PYTHON_PATH="$(abs_path "$PYTHON_PATH")" +"$PYTHON_PATH" --version >&2 || fail "Python not runnable: $PYTHON_PATH" + +if [[ -z "$WHEEL_DIR" && -d "$SCRIPT_ROOT/wheels" ]]; then WHEEL_DIR="$SCRIPT_ROOT/wheels"; fi +if [[ -n "$WHEEL_DIR" ]]; then WHEEL_DIR="$(abs_path "$WHEEL_DIR")"; fi + +venv_python() { + printf '%s\n' "$PROJECT_DIR/.venv/bin/python" +} + +ensure_venv() { + if [[ "$NO_VENV" == "1" ]]; then return; fi + local vpy + vpy="$(venv_python)" + if [[ ! -x "$vpy" ]]; then + progress venv + log_step "Create venv: $PROJECT_DIR/.venv" + "$PYTHON_PATH" -m venv "$PROJECT_DIR/.venv" + fi +} + +install_deps() { + local py="$1" + [[ "$SKIP_PIP_INSTALL" == "1" ]] && return + progress deps + log_step "Install desktop bridge dependencies" + "$py" -m pip install --upgrade pip setuptools wheel + local pkgs=( + "requests>=2.28" "beautifulsoup4>=4.12" "bottle>=0.12" "simple-websocket-server>=0.4" "aiohttp>=3.9" psutil + ) + if [[ -n "$EXTRA_PACKAGES" ]]; then + # shellcheck disable=SC2206 + pkgs+=( $EXTRA_PACKAGES ) + fi + if [[ -n "$WHEEL_DIR" && -d "$WHEEL_DIR" ]]; then + "$py" -m pip install --no-index --find-links "$WHEEL_DIR" "${pkgs[@]}" + else + log_warn "No wheel dir supplied; falling back to online pip install" + "$py" -m pip install "${pkgs[@]}" + fi +} + +ensure_mykey() { + local mykey="$PROJECT_DIR/mykey.py" + local tpl="$PROJECT_DIR/mykey_template.py" + if [[ -f "$mykey" ]]; then + log_ok "mykey.py exists" + elif [[ -f "$tpl" ]]; then + cp "$tpl" "$mykey" + log_warn "Created mykey.py from mykey_template.py. Please fill API keys before model calls." + else + log_warn "mykey.py and mykey_template.py are missing. Model config may need manual setup." + fi +} + +write_settings() { + local py="$1" + local settings_path="$HOME/.ga_desktop_settings.json" + "$py" - "$settings_path" "$py" "$PROJECT_DIR" <<'PY' +import json, pathlib, sys +settings_path, python_path, project_dir = sys.argv[1:4] +pathlib.Path(settings_path).write_text(json.dumps({"python_path": python_path, "project_dir": project_dir}, indent=2), encoding="utf-8") +PY + log_ok "Wrote desktop settings: $settings_path" +} + +start_bridge() { + local py="$1" + local bridge="$PROJECT_DIR/frontends/desktop_bridge.py" + [[ -f "$bridge" ]] || fail "desktop_bridge.py not found: $bridge" + export PYTHONPATH="$PROJECT_DIR:$PROJECT_DIR/frontends:${PYTHONPATH:-}" + exec "$py" "$bridge" +} + +launch_app() { + if [[ -n "$APP_PATH" ]]; then + open "$APP_PATH" + elif [[ -d "$SCRIPT_ROOT/../GenericAgent.app" ]]; then + open "$SCRIPT_ROOT/../GenericAgent.app" + else + log_warn "GenericAgent.app not found next to runtime; skip launch" + fi +} + +ensure_venv +RUNTIME_PY="$(venv_python)" +if [[ "$NO_VENV" == "1" ]]; then RUNTIME_PY="$PYTHON_PATH"; fi +install_deps "$RUNTIME_PY" +ensure_mykey +write_settings "$RUNTIME_PY" +progress done + +case "$MODE" in + PrepareOnly) log_ok "Prepare complete: $PROJECT_DIR" ;; + BridgeOnly) start_bridge "$RUNTIME_PY" ;; + Launch) launch_app ;; +esac diff --git a/frontends/desktop/packaging/scripts/macos/uninstall.command b/frontends/desktop/packaging/scripts/macos/uninstall.command new file mode 100644 index 000000000..0898a03b6 --- /dev/null +++ b/frontends/desktop/packaging/scripts/macos/uninstall.command @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# GenericAgent Desktop — portable uninstall (macOS). Double-clickable in Finder (.command). +# +# Removes everything THIS portable bundle put on the machine, then deletes the +# bundle folder itself: +# 1. Stop the bundle's processes (GUI + bridge/conductor/scheduler python) — +# only processes whose command line lives inside this bundle. +# 2. Remove the desktop alias (~/Desktop/GenericAgent.app) — only when it links +# into this bundle. +# 3. Remove ~/.ga_desktop_settings.json (shared settings). +# 4. Remove the WKWebView data for the app id under ~/Library. +# 5. Delete the bundle folder. +set -u + +BUNDLE="$(cd "$(dirname "$0")" && pwd)" +APP_ID="com.genericagent.app" + +echo "============================================================" +echo " GenericAgent Desktop - Uninstall" +echo "============================================================" +echo +echo "This will completely remove GenericAgent from this computer:" +echo " - stop its background services (bridge 14168 / conductor 8900)" +echo " - delete the desktop alias" +echo " - delete settings (~/.ga_desktop_settings.json)" +echo " - delete WebView data (~/Library/.../$APP_ID)" +echo " - delete THIS folder and everything in it:" +echo " $BUNDLE" +echo +echo "This cannot be undone." +echo +read -r -p "Type Y to uninstall, anything else to cancel: " CONFIRM +case "$CONFIRM" in + y|Y) ;; + *) echo; echo "Cancelled. Nothing was changed."; exit 0 ;; +esac + +echo +echo "==> Stopping GenericAgent backend services" +curl -fsS -m 3 -X POST "http://127.0.0.1:14168/services/bridge/exit" >/dev/null 2>&1 || true +sleep 1 + +# Kill any process whose command line lives inside this bundle (no /proc on macOS, +# so match on the full argv via ps; -ww disables column truncation so a deep bundle +# path is never cut off). Scoped to the bundle path → other installs untouched. +# Skip our own shell ($$) and its parent (the Terminal-spawned launcher). +selfpid=$$ +ps -axww -o pid=,command= 2>/dev/null | while read -r pid cmd; do + [ "$pid" = "$selfpid" ] && continue + [ "$pid" = "$PPID" ] && continue + case "$cmd" in + *"$BUNDLE"*) kill -9 "$pid" 2>/dev/null && echo " killed PID $pid" ;; + esac +done +echo "[OK] backend stopped" + +echo "==> Removing desktop alias" +link="$HOME/Desktop/GenericAgent.app" +if [ -L "$link" ]; then + case "$(readlink "$link")" in + "$BUNDLE"*) rm -f "$link" && echo "[OK] removed $link" ;; + *) echo " desktop alias points to another bundle; left in place" ;; + esac +else + echo " no desktop alias found" +fi + +echo "==> Removing settings file" +if [ -f "$HOME/.ga_desktop_settings.json" ]; then + rm -f "$HOME/.ga_desktop_settings.json" && echo "[OK] removed ~/.ga_desktop_settings.json" +else + echo " no settings file found" +fi + +echo "==> Removing WebView data" +for d in "$HOME/Library/WebKit/$APP_ID" \ + "$HOME/Library/Caches/$APP_ID" \ + "$HOME/Library/Application Support/$APP_ID" \ + "$HOME/Library/HTTPStorages/$APP_ID" \ + "$HOME/Library/Saved Application State/$APP_ID.savedState"; do + if [ -e "$d" ]; then rm -rf "$d" && echo "[OK] removed $d"; fi +done +rm -f "$HOME/Library/Preferences/$APP_ID.plist" 2>/dev/null || true + +echo "==> Removing the bundle folder" +cd /tmp || cd / +if rm -rf "$BUNDLE" 2>/dev/null && [ ! -e "$BUNDLE" ]; then + echo "[OK] removed $BUNDLE" +else + nohup bash -c 'for i in $(seq 1 20); do rm -rf "'"$BUNDLE"'" 2>/dev/null; [ -e "'"$BUNDLE"'" ] || exit 0; sleep 1; done' >/dev/null 2>&1 & + echo "[OK] bundle folder will be removed after exit: $BUNDLE" +fi + +echo +echo "GenericAgent has been uninstalled." diff --git a/frontends/desktop/packaging/scripts/windows/install_windows.ps1 b/frontends/desktop/packaging/scripts/windows/install_windows.ps1 new file mode 100644 index 000000000..e501d5b30 --- /dev/null +++ b/frontends/desktop/packaging/scripts/windows/install_windows.ps1 @@ -0,0 +1,388 @@ +<# +GenericAgent Desktop Windows setup script + +Usage examples: + powershell -ExecutionPolicy Bypass -File .\install_windows.ps1 + powershell -ExecutionPolicy Bypass -File .\install_windows.ps1 -ProjectDir D:\GenericAgent_Desktop -Mode PrepareOnly + powershell -ExecutionPolicy Bypass -File .\install_windows.ps1 -Mode BridgeOnly + powershell -ExecutionPolicy Bypass -File .\install_windows.ps1 -Mode NpmInstallOnly + powershell -ExecutionPolicy Bypass -File .\install_windows.ps1 -Mode DesktopBuildOnly + powershell -ExecutionPolicy Bypass -File .\install_windows.ps1 -Mode DesktopDevOnly + +Mirror examples (default to China mirrors for faster downloads): + # use the built-in defaults (Tsinghua PyPI + npmmirror) + powershell -ExecutionPolicy Bypass -File .\install_windows.ps1 + # use a different pip mirror + powershell -ExecutionPolicy Bypass -File .\install_windows.ps1 -PipIndexUrl https://mirrors.aliyun.com/pypi/simple/ + # disable mirrors, fall back to official PyPI / npm registry + powershell -ExecutionPolicy Bypass -File .\install_windows.ps1 -PipIndexUrl "" -NpmRegistry "" + +What this script does: + 1. Locate GenericAgent project dir, which must contain agentmain.py. + 2. Locate a supported Python, or create/use .venv. + 3. Install minimal Python dependencies for desktop bridge. + 4. Copy mykey_template.py to mykey.py if mykey.py is missing. + 5. Write %USERPROFILE%\.ga_desktop_settings.json for the Tauri shell. + 6. Optionally install npm/Tauri dependencies, build debug desktop exe, or start bridge/exe. + +Important: + - This script lives under test_workspace and is still a draft, but these modes have been smoke-tested here: + PrepareOnly, BridgeOnly/manual bridge smoke, NpmInstallOnly, DesktopBuildOnly, and debug exe GUI autostart. + - Development install and packaged-user install are not exactly the same. + Shared part: Python/env/deps/config preparation. + Different part: where files live, whether exe exists, whether Python is bundled. +#> + +param( + [string]$ProjectDir = "", + [string]$PythonPath = "", + [ValidateSet("Auto", "PrepareOnly", "BridgeOnly", "ExeOnly", "NpmInstallOnly", "DesktopDevOnly", "DesktopBuildOnly")] + [string]$Mode = "Auto", + [switch]$NoVenv, + [switch]$SkipPipInstall, + [switch]$SkipNpmInstall, + [switch]$SkipWebView2Check, + # Package mirrors. Default to China mirrors for speed; pass "" to use the official source. + [string]$PipIndexUrl = "https://pypi.tuna.tsinghua.edu.cn/simple", + [string]$NpmRegistry = "https://registry.npmmirror.com", + # Offline install: when set, pip installs from local wheels only (no network). Used by the portable bundle. + [string]$WheelDir = "", + # Extra packages to install beyond the core deps (e.g. "fastapi uvicorn websockets" for the conductor service). + [string]$ExtraPipPackages = "" +) + +$ErrorActionPreference = "Stop" + +function Write-Step([string]$msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan } +function Write-Ok([string]$msg) { Write-Host "[OK] $msg" -ForegroundColor Green } +function Write-Warn([string]$msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow } +function Fail([string]$msg) { throw "[ERROR] $msg" } + +function Resolve-ScriptRoot { + if ($PSScriptRoot) { return (Resolve-Path $PSScriptRoot).Path } + return (Get-Location).Path +} + +function Find-ProjectRoot([string]$startDir) { + if ($ProjectDir) { + $p = Resolve-Path $ProjectDir -ErrorAction Stop + if (Test-Path (Join-Path $p "agentmain.py")) { return $p.Path } + Fail "ProjectDir does not contain agentmain.py: $ProjectDir" + } + + $candidates = @( + (Get-Location).Path, + $startDir, + (Join-Path $startDir ".."), + (Join-Path $startDir "..\.."), + (Join-Path $startDir "..\..\.."), + "D:\GenericAgent_Desktop" + ) + + foreach ($c in $candidates) { + try { $rp = Resolve-Path $c -ErrorAction Stop } catch { continue } + if (Test-Path (Join-Path $rp.Path "agentmain.py")) { return $rp.Path } + } + Fail "Cannot locate GenericAgent project root. Pass -ProjectDir ." +} + +function Get-PythonVersionObject([string]$py) { + try { + $out = & $py -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')" 2>$null + if ($LASTEXITCODE -ne 0 -or -not $out) { return $null } + return [version]($out.Trim()) + } catch { return $null } +} + +function Test-SupportedPython([string]$py) { + $v = Get-PythonVersionObject $py + if (-not $v) { return $false } + return ($v.Major -eq 3 -and $v.Minor -ge 10 -and $v.Minor -lt 14) +} + +function Find-Python([string]$root) { + if ($PythonPath) { + if (Test-SupportedPython $PythonPath) { return (Resolve-Path $PythonPath).Path } + Fail "Specified Python is not supported. Need Python >=3.10,<3.14: $PythonPath" + } + + $portableCandidates = @( + (Join-Path $root ".portable\uv-python\python.exe"), + (Join-Path $root ".portable\python\python.exe"), + (Join-Path $root "python\python.exe") + ) + foreach ($p in $portableCandidates) { + if ((Test-Path $p) -and (Test-SupportedPython $p)) { return (Resolve-Path $p).Path } + } + + $cmds = @("python", "py") + foreach ($cmd in $cmds) { + try { + if ($cmd -eq "py") { + $probe = & py -3.12 -c "import sys; print(sys.executable)" 2>$null + if ($LASTEXITCODE -eq 0 -and $probe -and (Test-SupportedPython $probe.Trim())) { return $probe.Trim() } + $probe = & py -3.11 -c "import sys; print(sys.executable)" 2>$null + if ($LASTEXITCODE -eq 0 -and $probe -and (Test-SupportedPython $probe.Trim())) { return $probe.Trim() } + $probe = & py -3.10 -c "import sys; print(sys.executable)" 2>$null + if ($LASTEXITCODE -eq 0 -and $probe -and (Test-SupportedPython $probe.Trim())) { return $probe.Trim() } + } else { + $probe = & python -c "import sys; print(sys.executable)" 2>$null + if ($LASTEXITCODE -eq 0 -and $probe -and (Test-SupportedPython $probe.Trim())) { return $probe.Trim() } + } + } catch { } + } + + Fail "No supported Python found. Install Python 3.10-3.13, or pass -PythonPath." +} + +function Ensure-Venv([string]$root, [string]$basePython) { + if ($NoVenv) { return $basePython } + $venvDir = Join-Path $root ".venv" + $venvPy = Join-Path $venvDir "Scripts\python.exe" + if (-not (Test-Path $venvPy)) { + Write-Host "GAPROGRESS|venv" + Write-Step "Create virtual environment: $venvDir" + & $basePython -m venv $venvDir + if ($LASTEXITCODE -ne 0) { Fail "Failed to create venv." } + } + if (-not (Test-SupportedPython $venvPy)) { Fail "Venv Python is invalid: $venvPy" } + return (Resolve-Path $venvPy).Path +} + +function Install-Dependencies([string]$root, [string]$py) { + if ($SkipPipInstall) { Write-Warn "SkipPipInstall is set; dependencies are not installed."; return } + + # Extra packages (e.g. conductor service deps) appended to the core install. + $extra = @() + if ($ExtraPipPackages) { $extra = $ExtraPipPackages.Split(" ", [StringSplitOptions]::RemoveEmptyEntries) } + + # Offline mode (portable bundle): install from local wheels only, no network, no pip self-upgrade. + if ($WheelDir) { + $wd = (Resolve-Path $WheelDir -ErrorAction Stop).Path + Write-Ok "offline wheels: $wd" + if ($extra.Count) { Write-Ok "extra packages: $($extra -join ', ')" } + Write-Host "GAPROGRESS|deps" + Write-Step "Install GenericAgent dependencies and desktop bridge extras (offline)" + # Install deps directly (NOT an editable -e of the source): an editable install bakes the + # project's absolute path into a .pth. With -NoVenv (deps go into the relocatable embedded + # python) this keeps the portable bundle movable. The bridge adds the source to sys.path + # itself (ensure_ga_import_path), so the project itself need not be installed. + & $py -m pip install --no-index --find-links $wd "requests>=2.28" "beautifulsoup4>=4.12" "bottle>=0.12" "simple-websocket-server>=0.4" "aiohttp>=3.9" psutil @extra + if ($LASTEXITCODE -ne 0) { Fail "offline pip install failed (check wheels dir)." } + Write-Host "GAPROGRESS|done" + return + } + + # Online mode: use a pip index mirror when -PipIndexUrl is set (default: Tsinghua). Pass -PipIndexUrl "" for official PyPI. + $pipIndexArgs = @() + if ($PipIndexUrl) { + $pipIndexArgs = @("-i", $PipIndexUrl) + Write-Ok "pip index mirror: $PipIndexUrl" + } else { + Write-Warn "No pip mirror set; using official PyPI." + } + Write-Step "Upgrade pip" + & $py -m pip install @pipIndexArgs --upgrade pip + if ($LASTEXITCODE -ne 0) { Fail "pip upgrade failed." } + + Write-Step "Install GenericAgent minimal package and desktop bridge extras" + # pyproject.toml already includes: requests, beautifulsoup4, bottle, simple-websocket-server, aiohttp. + # desktop_bridge.py additionally imports psutil. + & $py -m pip install @pipIndexArgs -e $root psutil @extra + if ($LASTEXITCODE -ne 0) { Fail "pip install failed." } +} + +function Ensure-MyKey([string]$root) { + $mykey = Join-Path $root "mykey.py" + $tpl = Join-Path $root "mykey_template.py" + if (Test-Path $mykey) { + Write-Ok "mykey.py exists" + return + } + if (Test-Path $tpl) { + Copy-Item $tpl $mykey + Write-Warn "Created mykey.py from mykey_template.py. User still needs to fill API keys." + } else { + Write-Warn "mykey.py and mykey_template.py are missing. Model config may not work." + } +} + +function Write-DesktopSettings([string]$root, [string]$py) { + $settingsPath = Join-Path $env:USERPROFILE ".ga_desktop_settings.json" + $obj = [ordered]@{ + python_path = $py + project_dir = $root + bridge_script = (Join-Path $root "frontends\desktop_bridge.py") + } + $json = $obj | ConvertTo-Json -Depth 5 + # PowerShell 5.1 `Set-Content -Encoding UTF8` writes a UTF-8 BOM. + # Rust serde_json does not accept BOM at the beginning, causing the Tauri shell + # to ignore this settings file and auto-discover the wrong project directory. + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($settingsPath, $json, $utf8NoBom) + Write-Ok "Wrote desktop settings: $settingsPath" +} + +function Test-WebView2Installed { + if ($SkipWebView2Check) { return } + Write-Step "Check Microsoft Edge WebView2 Runtime" + $keys = @( + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}", + "HKLM:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}", + "HKCU:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" + ) + foreach ($k in $keys) { + if (Test-Path $k) { Write-Ok "WebView2 Runtime detected"; return } + } + Write-Warn "WebView2 Runtime not detected. Tauri desktop window may fail on clean Windows." + Write-Warn "Download: https://developer.microsoft.com/microsoft-edge/webview2/" +} + +function Find-DesktopDir([string]$root) { + $desktop = Join-Path $root "frontends\desktop" + if (-not (Test-Path (Join-Path $desktop "src-tauri\tauri.conf.json"))) { + Fail "Tauri desktop dir not found: $desktop" + } + return (Resolve-Path $desktop).Path +} + +function Ensure-NodeAndNpm { + $node = Get-Command node -ErrorAction SilentlyContinue + $npm = Get-Command npm -ErrorAction SilentlyContinue + if (-not $node) { Fail "node is not available in PATH. Install Node.js 20+ for desktop dev/build." } + if (-not $npm) { Fail "npm is not available in PATH. Install Node.js/npm for desktop dev/build." } + $nodeVer = & node --version + $npmVer = & cmd /c npm --version + Write-Ok "Node: $nodeVer; npm: $npmVer" +} + +function Install-DesktopNpm([string]$root) { + if ($SkipNpmInstall) { Write-Warn "SkipNpmInstall is set; desktop npm dependencies are not installed."; return } + $desktop = Find-DesktopDir $root + Ensure-NodeAndNpm + # Use an npm registry mirror when -NpmRegistry is set (default: npmmirror). Pass -NpmRegistry "" for official registry. + $npmRegArgs = @() + if ($NpmRegistry) { + $npmRegArgs = @("--registry", $NpmRegistry) + Write-Ok "npm registry mirror: $NpmRegistry" + } + Write-Step "Install Tauri desktop npm dependencies: $desktop" + Push-Location $desktop + try { + & cmd /c npm install @npmRegArgs + if ($LASTEXITCODE -ne 0) { Fail "npm install failed in $desktop" } + & cmd /c npx tauri --version + if ($LASTEXITCODE -ne 0) { Fail "npx tauri --version failed after npm install" } + } finally { + Pop-Location + } +} + +function Build-DesktopDebug([string]$root) { + $desktop = Find-DesktopDir $root + Install-DesktopNpm $root + Write-Step "Build Tauri debug desktop exe" + Push-Location $desktop + try { + & cmd /c npx tauri build --debug + if ($LASTEXITCODE -ne 0) { Fail "Tauri debug build failed." } + } finally { + Pop-Location + } + $debugExe = Join-Path $desktop "src-tauri\target\debug\ga-desktop.exe" + if (Test-Path $debugExe) { Write-Ok "Debug exe: $debugExe" } else { Write-Warn "Debug exe not found at expected path: $debugExe" } +} + +function Start-DesktopDev([string]$root) { + $desktop = Find-DesktopDir $root + Install-DesktopNpm $root + Write-Step "Start Tauri desktop dev shell" + Write-Warn "This keeps running in current console. If status checks fail, bypass proxy for http://127.0.0.1:14168/status." + Push-Location $desktop + try { + & cmd /c npx tauri dev -- --dev + } finally { + Pop-Location + } +} + +function Find-PackagedExe([string]$root) { + $candidates = @( + (Join-Path $root "frontends\GenericAgent.exe"), + (Join-Path $root "frontends\desktop\src-tauri\target\release\GenericAgent.exe"), + (Join-Path $root "frontends\desktop\src-tauri\target\release\ga-desktop.exe"), + (Join-Path $root "frontends\desktop\src-tauri\target\debug\ga-desktop.exe") + ) + foreach ($p in $candidates) { if (Test-Path $p) { return (Resolve-Path $p).Path } } + return "" +} + +function Start-Bridge([string]$root, [string]$py) { + $bridge = Join-Path $root "frontends\desktop_bridge.py" + if (-not (Test-Path $bridge)) { Fail "desktop_bridge.py not found: $bridge" } + Write-Step "Start bridge: $bridge" + Write-Warn "This keeps running in current console. Browse http://127.0.0.1:14168/status to check." + $env:PYTHONPATH = "$root;$(Join-Path $root 'frontends');$env:PYTHONPATH" + & $py $bridge +} + +function Start-Exe([string]$root) { + $exe = Find-PackagedExe $root + if (-not $exe) { Fail "Packaged GenericAgent.exe not found. Use -Mode BridgeOnly or build Tauri first." } + Write-Step "Start packaged desktop exe" + Start-Process -FilePath $exe -WorkingDirectory (Split-Path $exe -Parent) + Write-Ok "Started: $exe" +} + +Write-Step "Resolve project root" +$scriptRoot = Resolve-ScriptRoot +$root = Find-ProjectRoot $scriptRoot +Write-Ok "Project root: $root" + +Write-Step "Resolve Python" +$basePy = Find-Python $root +Write-Ok "Base Python: $basePy" + +$py = Ensure-Venv $root $basePy +Write-Ok "Runtime Python: $py" + +Install-Dependencies $root $py +Ensure-MyKey $root +Write-DesktopSettings $root $py +Test-WebView2Installed + +if ($Mode -eq "PrepareOnly") { + Write-Ok "Preparation finished. No app started because -Mode PrepareOnly was used." + exit 0 +} + +if ($Mode -eq "BridgeOnly") { + Start-Bridge $root $py + exit 0 +} + +if ($Mode -eq "NpmInstallOnly") { + Install-DesktopNpm $root + Write-Ok "Desktop npm setup finished." + exit 0 +} + +if ($Mode -eq "DesktopBuildOnly") { + Build-DesktopDebug $root + Write-Ok "Desktop debug build finished." + exit 0 +} + +if ($Mode -eq "DesktopDevOnly") { + Start-DesktopDev $root + exit 0 +} + +if ($Mode -eq "ExeOnly") { + Start-Exe $root + exit 0 +} + +# Auto mode: prefer packaged exe if present, otherwise start bridge for source/dev tree. +$exe = Find-PackagedExe $root +if ($exe) { Start-Exe $root } else { Start-Bridge $root $py } diff --git a/frontends/desktop/packaging/scripts/windows/uninstall.bat b/frontends/desktop/packaging/scripts/windows/uninstall.bat new file mode 100644 index 000000000..077652ec4 --- /dev/null +++ b/frontends/desktop/packaging/scripts/windows/uninstall.bat @@ -0,0 +1,37 @@ +@echo off +setlocal +cd /d "%~dp0" + +echo ============================================================ +echo GenericAgent Desktop - Uninstall +echo ============================================================ +echo. +echo This will completely remove GenericAgent from this computer: +echo - stop its background services (ports 14168 / 8900) +echo - delete the desktop shortcut +echo - delete settings (%%USERPROFILE%%\.ga_desktop_settings.json) +echo - delete THIS folder and everything in it: +echo "%~dp0" +echo. +echo This cannot be undone. +echo. +set /p CONFIRM="Type Y to uninstall, anything else to cancel: " +if /i not "%CONFIRM%"=="Y" ( + echo. + echo Cancelled. Nothing was changed. + pause + exit /b 0 +) + +echo. +rem %~dp0 ends with a backslash; passing "...\dir\" makes the trailing \" escape the closing +rem quote, so PowerShell receives a path with a literal quote ("Illegal characters in path"). +rem Strip the trailing backslash before passing the bundle dir. +set "BUNDLE=%~dp0" +if "%BUNDLE:~-1%"=="\" set "BUNDLE=%BUNDLE:~0,-1%" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0runtime\uninstall_windows.ps1" -BundleDir "%BUNDLE%" + +echo. +echo You can close this window now. +timeout /t 3 >nul +exit /b 0 diff --git a/frontends/desktop/packaging/scripts/windows/uninstall_windows.ps1 b/frontends/desktop/packaging/scripts/windows/uninstall_windows.ps1 new file mode 100644 index 000000000..e2262585d --- /dev/null +++ b/frontends/desktop/packaging/scripts/windows/uninstall_windows.ps1 @@ -0,0 +1,146 @@ +<# +GenericAgent Desktop — portable uninstall (Windows). + +Removes everything THIS portable bundle put on the machine, then deletes the +bundle folder itself: + 1. Stop the bundle's backend processes (bridge 14168 / conductor 8900 / scheduler) + — only processes whose executable lives inside this bundle, so other bundles + on the same machine are left alone. + 2. Remove the desktop shortcut (GenericAgent.lnk) — only when it points into + this bundle. + 3. Remove ~/.ga_desktop_settings.json (shared settings; other bundles rebuild it + automatically on next launch). + 4. Schedule deletion of the bundle folder after this script exits (a folder + cannot delete itself while code runs inside it). + +Invoked by uninstall.bat (which confirms with the user first). Not meant to be +run directly without -BundleDir. +#> + +param( + [Parameter(Mandatory = $true)] + [string]$BundleDir +) + +$ErrorActionPreference = "SilentlyContinue" + +function Write-Step([string]$m) { Write-Host "==> $m" -ForegroundColor Cyan } +function Write-Ok([string]$m) { Write-Host "[OK] $m" -ForegroundColor Green } +function Write-Info([string]$m) { Write-Host " $m" -ForegroundColor Gray } + +# Normalize the bundle dir to an absolute path with a trailing separator for prefix checks. +# Defensive: strip stray quotes / trailing separators a caller may have leaked in (e.g. the +# classic "%~dp0" trailing-backslash-before-quote bug). +$BundleDir = $BundleDir.Trim().Trim('"').TrimEnd('\', '/') +try { $bundle = (Resolve-Path -LiteralPath $BundleDir).Path } catch { $bundle = $BundleDir } +$bundlePrefix = ($bundle.TrimEnd('\') + '\').ToLowerInvariant() + +function Path-IsInsideBundle([string]$p) { + if (-not $p) { return $false } + try { $rp = (Resolve-Path -LiteralPath $p -ErrorAction Stop).Path } catch { $rp = $p } + return $rp.ToLowerInvariant().StartsWith($bundlePrefix) +} + +# ── 1. Graceful backend shutdown, then force-kill bundle-owned processes ────── +Write-Step "Stopping GenericAgent backend services" + +# Best-effort graceful exit: tell the bridge to stop its managed extras and quit. +try { + Invoke-WebRequest -Uri "http://127.0.0.1:14168/services/bridge/exit" -Method Post ` + -TimeoutSec 3 -UseBasicParsing | Out-Null + Start-Sleep -Milliseconds 800 +} catch { } + +# Force-kill anything still listening on our ports, but ONLY if the owning process +# executable lives inside this bundle (don't disturb a second installed copy). +foreach ($port in 14168, 8900) { + foreach ($conn in (Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue)) { + $proc = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue + if ($proc -and (Path-IsInsideBundle $proc.Path)) { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + Write-Info "killed PID $($proc.Id) ($($proc.ProcessName)) on port $port" + } elseif ($proc) { + Write-Info "port $port held by a process outside this bundle (PID $($proc.Id)); left running" + } + } +} + +# Kill ANY process whose executable image lives inside this bundle, regardless of name +# (GenericAgent.exe, python.exe, or any child tool it spawned). Limiting to the bundle path +# means we never touch a second installed copy. +foreach ($p in (Get-Process -ErrorAction SilentlyContinue)) { + if (Path-IsInsideBundle $p.Path) { + Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue + Write-Info "killed PID $($p.Id) ($($p.ProcessName))" + } +} +Write-Ok "backend stopped" + +# ── 2. Desktop shortcut (only if it targets this bundle) ───────────────────── +Write-Step "Removing desktop shortcut" +$desktop = [Environment]::GetFolderPath('Desktop') +$lnk = Join-Path $desktop 'GenericAgent.lnk' +if (Test-Path -LiteralPath $lnk) { + $target = $null + try { + $ws = New-Object -ComObject WScript.Shell + $target = $ws.CreateShortcut($lnk).TargetPath + } catch { } + if ((-not $target) -or (Path-IsInsideBundle $target)) { + Remove-Item -LiteralPath $lnk -Force -ErrorAction SilentlyContinue + Write-Ok "removed $lnk" + } else { + Write-Info "desktop shortcut points to another bundle; left in place" + } +} else { + Write-Info "no desktop shortcut found" +} + +# ── 3. Shared settings file ────────────────────────────────────────────────── +Write-Step "Removing settings file" +$settings = Join-Path $env:USERPROFILE '.ga_desktop_settings.json' +if (Test-Path -LiteralPath $settings) { + Remove-Item -LiteralPath $settings -Force -ErrorAction SilentlyContinue + Write-Ok "removed $settings" +} else { + Write-Info "no settings file found" +} + +# ── 3b. WebView2 data dir (cache + localStorage) ───────────────────────────── +# Tauri creates %LOCALAPPDATA%\com.genericagent.app\ (EBWebView: HTTP cache + localStorage) +# outside the bundle, keyed by the app identifier. Only the Tauri desktop shell uses it (the +# project's other frontends — qt/tui/conductor — do not), so removing it is safe. Other +# GenericAgent bundles share the same identifier and would just rebuild it on next launch. +Write-Step "Removing WebView2 data" +$wv = Join-Path $env:LOCALAPPDATA 'com.genericagent.app' +if (Test-Path -LiteralPath $wv) { + Remove-Item -LiteralPath $wv -Recurse -Force -ErrorAction SilentlyContinue + if (Test-Path -LiteralPath $wv) { Write-Info "WebView2 data partially locked; some files remain" } + else { Write-Ok "removed $wv" } +} else { + Write-Info "no WebView2 data found" +} + +# ── 4. Schedule deletion of the bundle folder ──────────────────────────────── +# The folder cannot remove itself while this script (and the uninstall.bat that +# launched it) run from inside it. Spawn a detached cmd that waits for our process +# tree to fully exit, then retries the delete a few times in case a handle lingers. +Write-Step "Scheduling removal of the bundle folder" +$deleter = @" +cd /d "%TEMP%" +for /l %%i in (1,1,20) do ( + rd /s /q "$bundle" 2>nul + if not exist "$bundle" goto done + ping 127.0.0.1 -n 2 >nul +) +:done +"@ +$deleterPath = Join-Path $env:TEMP ("ga_uninstall_{0}.bat" -f ([guid]::NewGuid().ToString('N'))) +Set-Content -LiteralPath $deleterPath -Value $deleter -Encoding ASCII +# Start detached so it survives this script + uninstall.bat exiting. The retry loop +# (20 tries x ~1s) covers the brief window where the parent processes release handles. +Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "`"$deleterPath`"" -WindowStyle Hidden | Out-Null +Write-Ok "bundle folder will be deleted after exit: $bundle" + +Write-Host "" +Write-Host "GenericAgent has been uninstalled." -ForegroundColor Green diff --git a/frontends/desktop/public/assets/fonts/README.md b/frontends/desktop/public/assets/fonts/README.md new file mode 100644 index 000000000..f8e5a57e8 --- /dev/null +++ b/frontends/desktop/public/assets/fonts/README.md @@ -0,0 +1,34 @@ +# Desktop Font Assets + +These font files are the offline Latin bundle for the vanilla desktop shell. + +## Files + +- `azonix-wordmark.woff2` + - Role: brand wordmark only + - Source: converted on 2026-05-25 from the local owner-installed file at `~/Library/Fonts/azonix/Azonix.otf` + - License: owner-provided / verify before external redistribution + - Note: this repo currently treats Azonix as a project-local brand asset + +- `lexend-latin.woff2` + - Role: English titles and navigation + - Source: Google Fonts CSS2 API, specimen page + - Downloaded: 2026-05-25 + - License: SIL Open Font License 1.1 + +- `noto-sans-latin.woff2` + - Role: English body copy and default Latin UI text + - Source: Google Fonts CSS2 API, specimen page + - Downloaded: 2026-05-25 + - License: SIL Open Font License 1.1 + +- `jetbrains-mono-latin.woff2` + - Role: config/value dense controls and numeric surfaces + - Source: Google Fonts CSS2 API, specimen page + - Downloaded: 2026-05-25 + - License: SIL Open Font License 1.1 + +## Notes + +- Only Latin subsets are bundled here. Chinese UI text continues to use the existing system fallback stack. +- Runtime does not fetch fonts from a CDN. `frontends/desktop/static/assets/fonts/fonts.css` is the only font entrypoint for the vanilla shell. diff --git a/frontends/desktop/public/assets/fonts/azonix-wordmark.woff2 b/frontends/desktop/public/assets/fonts/azonix-wordmark.woff2 new file mode 100644 index 000000000..947bfbebc Binary files /dev/null and b/frontends/desktop/public/assets/fonts/azonix-wordmark.woff2 differ diff --git a/frontends/desktop/public/assets/fonts/fonts.css b/frontends/desktop/public/assets/fonts/fonts.css new file mode 100644 index 000000000..8103b351d --- /dev/null +++ b/frontends/desktop/public/assets/fonts/fonts.css @@ -0,0 +1,21 @@ +@font-face { + font-family: "GA Azonix"; + src: + url("./azonix-wordmark.woff2") format("woff2"), + local("Azonix"), + local("Azonix Regular"), + local("Azonix-Regular"); + font-style: normal; + font-weight: 400; + font-display: swap; + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+20AC, U+2122, U+2212, U+FEFF, U+FFFD; +} + +@font-face { + font-family: "GA JetBrains Mono"; + src: url("./jetbrains-mono-latin.woff2") format("woff2"); + font-style: normal; + font-weight: 400 600; + font-display: swap; + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/frontends/desktop/public/assets/fonts/jetbrains-mono-latin.woff2 b/frontends/desktop/public/assets/fonts/jetbrains-mono-latin.woff2 new file mode 100644 index 000000000..2ca6ac605 Binary files /dev/null and b/frontends/desktop/public/assets/fonts/jetbrains-mono-latin.woff2 differ diff --git a/frontends/desktop/public/assets/fonts/lexend-latin.woff2 b/frontends/desktop/public/assets/fonts/lexend-latin.woff2 new file mode 100644 index 000000000..0968c152f Binary files /dev/null and b/frontends/desktop/public/assets/fonts/lexend-latin.woff2 differ diff --git a/frontends/desktop/public/assets/fonts/noto-sans-latin.woff2 b/frontends/desktop/public/assets/fonts/noto-sans-latin.woff2 new file mode 100644 index 000000000..e08293045 Binary files /dev/null and b/frontends/desktop/public/assets/fonts/noto-sans-latin.woff2 differ diff --git a/frontends/desktop/public/assets/ga-logo.svg b/frontends/desktop/public/assets/ga-logo.svg new file mode 100644 index 000000000..a4d456c14 --- /dev/null +++ b/frontends/desktop/public/assets/ga-logo.svg @@ -0,0 +1,58 @@ + + GenericAgent + + + diff --git a/frontends/desktop/public/fallback.html b/frontends/desktop/public/fallback.html new file mode 100644 index 000000000..0be15ecb4 --- /dev/null +++ b/frontends/desktop/public/fallback.html @@ -0,0 +1,267 @@ + + + + + +GenericAgent — Fix startup + + + +
    +

    修复启动问题

    +

    检查下面的位置,然后重试。你的记忆、会话和配置不会受到影响。

    + + + + + + + + + +
    + 诊断信息 +
    + + +
    +
    GenericAgent Desktop startup diagnostics
    +

    诊断信息包含本机路径和错误日志,不包含 API Key、会话或记忆内容。

    +
    +
    + + + diff --git a/frontends/desktop/public/i18n.js b/frontends/desktop/public/i18n.js new file mode 100644 index 000000000..0c90a9f9e --- /dev/null +++ b/frontends/desktop/public/i18n.js @@ -0,0 +1,496 @@ +(() => { + const I18N = { + zh: { + 'app.title': 'GenericAgent 桌面版', + 'brand.sub': '桌面终端', + 'nav.chat': '聊天', 'nav.services': '后台服务', 'nav.channels': '消息通道', 'nav.status': '状态面板', + 'nav.collab': '指挥家', 'nav.token': '用量', + 'foot.settings': '配置', 'foot.ver': 'GenericAgent · 桌面版', + 'chat.startTitle': '开始对话', 'chat.startSub': '直接输入,或点预设功能一键启动', + 'preset.butler.t': '指挥家', 'preset.butler.d': '复杂任务自动拆解,只需查看进度和简报', + 'preset.plan.t': 'Plan 模式', 'preset.plan.d': '加载 Plan SOP,按探索→规划→执行→验证流程', + 'preset.goal.t': 'Goal 模式', 'preset.goal.d': '设定目标,自主完成', + 'preset.autonomous.t': '自主行动', 'preset.autonomous.d': '按 SOP 规划/执行任务,产出报告(reflect/autonomous.py 同源)', + 'preset.hive.t': 'Hive 协作', 'preset.hive.d': '多 worker 协同攻坚', + 'preset.review.t': '深度复核', 'preset.review.d': '挑刺式质量把关', + 'preset.findwork.t': '找点事做', 'preset.findwork.d': '分析当前情况,推荐一批让你感兴趣的 TODO', + 'preset.mine.t': '我的·周报', 'preset.mine.d': '自定义:抓本周提交并写周报', + 'preset.add.t': '自定义', 'preset.add.d': '任意一句话存为功能', + 'composer.placeholder': 'GA 能帮你做些什么?', + 'search.placeholder': '搜索会话…', 'conv.new': '新对话', + 'ctx.pin': '置顶', 'ctx.unpin': '取消置顶', 'ctx.rename': '重命名', 'ctx.del': '删除', + 'common.close': '关闭', 'common.more': '更多', 'common.optional': '选填', 'common.save': '保存', + 'modal.preset': '预设功能', 'modal.addModel': '添加模型', 'modal.editModel': '编辑模型', 'modal.settings': '配置', + 'modal.customPreset': '自定义预设', + 'modal.editCustomPreset': '编辑任务', + 'customPreset.titlePh': '标题,例如「写周报」', + 'customPreset.promptPh': 'Prompt 内容,发送时会作为消息提交', + 'customPreset.empty': '标题和 Prompt 不能为空', + 'customPreset.removeTitle': '删除', + 'customPreset.editTitle': '编辑', + 'builtinPreset.restoreBtn': '恢复默认预设', + 'set.appearance': '外观', 'set.plainUi': '素色', 'set.fontSize': '聊天字号', 'set.lang': '语言', 'set.model': '模型', 'set.addModel': '添加模型', 'set.features': '功能', 'set.importMykey': '导入已有模型配置(mykey.py)', 'set.exportMykey': '导出当前模型配置', 'set.importMemory': '导入已有记忆与会话记录(选择 GenericAgent 根目录)', 'set.gaSource': '接入独立 GenericAgent 源码(把桌面版当作壳)', 'set.gaSourceClear': '取消接入,改用内置版本', 'set.gaSourceCurrent': '当前接入', 'set.serviceManager': '后台服务管理', + 'shortcut.askConfirm': '是否在桌面创建 GenericAgent 快捷方式?', + 'appearance.light': '浅色', 'appearance.dark': '深色', + 'set.noModels': '暂无模型,点击下方添加', + 'lang.zh': '简体中文', 'lang.en': 'English', + 'model.name': '备注', 'model.namePh': '会显示在模型列表', + 'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': '留空则保持原 Key 不变', + 'model.apibase': 'API 地址', 'model.apibasePh': 'https://.../v1/messages', + 'model.protocol': '协议', 'model.protocolPick': '请选择…', 'model.protocolOai': 'OpenAI 兼容 (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)', + 'model.stream': '响应方式', 'model.streamOn': '流式', 'model.streamOff': '非流式', + 'model.model': '模型', 'model.modelPh': 'model 参数名', + 'model.modelHint': '须与中转站/官方文档中的 model 字段完全一致', + 'model.retries': '重试 (次)', 'model.connTimeout': '连接超时 (s)', 'model.readTimeout': '读取超时 (s)', + 'model.save': '保存', 'common.cancel': '取消', 'common.confirm': '确认', 'common.edit': '编辑', 'common.delete': '删除', + 'pq.title': '快速接入官方模型', 'pq.sub': '填好 API Key 即可使用', 'pq.toggle': '展开 / 收起', + 'pq.deepseekDesc': '官方 API · OpenAI 兼容', 'pq.qwenDesc': '通义千问 · 阿里云百炼', + 'guide.step1': '点击下方链接,登录后创建并复制 API Key', + 'guide.step2': '把 Key 粘贴到下方「API Key」输入框', + 'guide.step3': '点击保存,即可在模型列表中选用', + 'guide.prefillTip': '已为你预填 API 地址、协议与模型,可按需修改', + 'guide.getKey': '获取 {name} 的 API Key', 'guide.copy': '复制链接', 'guide.copied': '链接已复制', + 'err.modelSave': '保存失败', 'err.modelSwitch': '切换模型失败', 'err.modelRequired': '请填写模型、API Key 和 API 地址', + 'err.modelDelete': '删除失败', 'err.modelDeleteLast': '至少保留一个模型', + 'confirm.modelDelete': '确定删除该模型配置?', + 'model.aggregation': '渠道组(自动故障转移)', 'model.aggregationShort': '渠道组', 'model.aggregationDesc': '按顺序尝试,失败自动切换到下一个', + 'model.emptyMixin': '尚未加入模型', + 'model.addToMixin': '加入渠道组', 'model.inMixin': '已在渠道组', 'model.removeFromMixin': '移出渠道组', 'model.alreadyInMixin': '已在渠道组中', 'model.dragReorder': '拖拽调整顺序', + 'err.mixinFailed': '操作失败', + 'page.services.title': '后台服务', 'page.services.sub': 'IM 消息通道与后台进程,集中查看、启停与日志', + 'page.channels.title': '消息通道', 'page.channels.sub': '后台 IM 进程:列表、启停与日志(同 hub.pyw)', + 'page.status.title': '状态面板', 'page.status.sub': 'hub.pyw 管理的后台进程/服务,集中查看与启停', + 'page.collab.title': '指挥家', 'page.collab.sub': '交代目标,自动拆活与跟进', + 'collab.progressTitle': '分工进度', + 'collab.progressEmpty': '还没有任务在执行。告诉指挥家你的目标后,这里会显示拆分后的处理进度。', + 'collab.placeholder': '请对指挥家描述你想完成的目标', + 'collab.guideTitle': '把要完成的事告诉指挥家', + 'collab.guideWhen': '适合需要多步处理、要花一些时间才能完成的目标。日常聊天和快问快答,请用左侧「聊天」。', + 'collab.guideStep1t': '描述目标', + 'collab.guideStep1d': '在聊天框里写下你想做的事,发给指挥家', + 'collab.guideStep2t': '自动拆解', + 'collab.guideStep2d': '指挥家自动拆解、分配任务,实时监督和调度', + 'collab.guideStep3t': '交付摘要', + 'collab.guideStep3d': '指挥家根据执行状态,呈上任务简报', + 'collab.guideStep4t': '随时调整', + 'collab.guideStep4d': '随时补充要求或细节,指挥家都会处理', + 'collab.chipProgress': '现在进展如何?', + 'collab.chipPause': '先暂停当前任务', + 'collab.chipSummary': '总结一下目前的结果', + 'collab.showProgressTitle': '查看分工进度', + 'collab.statRunning': '进行中', + 'collab.statDone': '已完成', + 'collab.plusMenu': '更多操作', + 'collab.switchMode': '切换模式', + 'collab.typing': '指挥家正在处理', + 'collab.offline': '无法连接指挥家服务,请确认后端已启动。', + 'collab.retry': '重试', + 'collab.reconnect': '连接断开,正在重连… 已保留上次任务进度。', + 'collab.reconnectIn': '{n} 秒后重试', + 'collab.stRunning': '执行中', 'collab.stReported': '已回报', 'collab.stPaused': '已暂停', + 'collab.stFailed': '遇到问题', 'collab.stTerminated': '已终止', + 'collab.summaryRunning': '正在处理中…', 'collab.summaryWait': '等待回报', + 'collab.taskFallback': '任务 {n}', + 'collab.timeJust': '刚刚', + 'collab.timeSec': '{n} 秒前', + 'collab.timeMin': '{n} 分钟前', + 'collab.timeHr': '{n} 小时前', + 'collab.timeDay': '{n} 天前', + 'page.token.title': '用量', 'page.token.sub': '每会话与累计用量及缓存率', + 'status.connecting': '正在连接…', 'status.ready': '服务在线', 'status.running': '处理中', + 'status.disconnected': '服务离线', 'status.stopped': '已停止', 'status.idle': '待命', + 'conv.emptyList': '暂无会话,点「+ 新对话」开始', 'conv.defaultTitle': '新对话', + 'err.bridge': '服务未响应', 'err.newSession': '新建会话失败', 'err.poll': '轮询失败', 'err.stop': '停止失败', + 'err.interruptTimeout': '等待上一轮停止超时,请稍后再试', + 'sys.interruptPrev.hint': '已停止上一轮,正在处理新消息', + 'chat.interrupting': '正在停止上一轮…', + 'chat.sessionLoading': '正在加载会话…', + 'sys.stopRequested': '已请求停止', + 'slash.help': '可用命令:\n/new 新会话 /clear 清屏 /stop 停止 /settings 设置', + 'slash.unknown': '未知命令', + 'upload.hint': '上传文件:选择 / 拖拽 / 粘贴', + 'upload.button': '上传文件', + 'upload.tooLarge': '文件过大或数量超限', 'upload.empty': '跳过空文件', + 'upload.failed': '上传失败', + 'err.charLimit': '已达字数上限({n}),发送时将自动截断', 'err.charLimitReached': '已达字数上限({n})', 'err.numMax': '不能超过 {n}', + 'file.openFailed': '无法打开文件', + 'file.kindGeneric': '文件', + 'file.kindDoc': '文档', + 'file.kindSheet': '表格', + 'file.kindSlide': '幻灯片', + 'file.kindCode': '代码', + 'file.kindArchive': '压缩包', + 'file.kindAudio': '音频', + 'file.kindVideo': '视频', + 'upload.removeTitle': '移除', + 'upload.dropHint': '松开以上传文件', + 'lightbox.closeTitle': '关闭', + 'fold.thinking': '思考', 'fold.tool': '工具调用', 'fold.toolResult': '工具结果', 'fold.llm': 'LLM Running', 'fold.turn': '第 {n} 轮', + 'plan.header': '计划 ({done}/{total})', 'plan.complete': '✓ 计划完成 ({n}/{n})', + 'plan.running': '计划执行中', 'plan.completeTitle': '计划完成', + 'plan.placeholder': '计划模式已激活', 'plan.waiting': '等待写入 {path} …', 'plan.overflow': '还有 {n} 项', + 'plan.current': '当前', 'plan.fold': '折叠', 'plan.collapse': '收起', 'plan.expand': '展开', 'plan.details': '详情', 'plan.dismiss': '不再显示', + 'plan.capsuleRunning': '运行中', 'plan.capsuleComplete': '已完成', + 'timing.elapsed': '已运行 {t}', + 'model.auto': '自动选择', + 'model.menuLabel': '选择模型', + 'chip.plan': 'Plan', + 'chip.auto': 'Auto', + 'ch.wechat': '微信', 'ch.wecom': '企业微信', 'ch.lark': '飞书', 'ch.dingtalk': '钉钉', + 'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord', + 'ch.loading': '加载中…', 'ch.empty': '未发现 IM 进程脚本', + 'ch.logEmpty': '暂无日志', + 'err.channelLoad': '加载失败', 'err.channelStart': '启动失败', 'err.channelStop': '停止失败', + 'err.mykeyImport': '导入模型配置失败', + 'err.mykeyExport': '导出模型配置失败', + 'err.channelNotConfigured': '请先在 mykey.py 中配置该平台', + 'sys.channelStarted': '已启动', 'sys.channelStopped': '已停止', + 'modal.channelLogs': '进程日志', + 'modal.mykeyConfig': 'mykey.py 配置', + 'sys.configSaved': '配置已保存', + 'sys.mykeyImported': '模型配置已导入', + 'sys.mykeyExported': '模型配置已导出', + 'sys.memoryImported': '记忆已导入', + 'err.memoryImport': '导入记忆失败', + 'sys.memoryImportBackup': '原记忆已备份至', + 'sys.memorySessions': '会话', + 'sys.gaSourcePickTitle': '选择要接入的 GenericAgent 源码根目录(含 agentmain.py)', + 'sys.gaSourceSwitching': '正在切换并重启后端...', + 'sys.gaSourceSet': '已接入独立 GenericAgent', + 'sys.gaSourceCleared': '已取消接入,恢复内置版本', + 'err.gaSourceSet': '接入失败', + 'err.gaSourceDesktopOnly': '此功能仅在桌面版中可用', + 'sys.memoryPickTitle': '选择 GenericAgent 根目录(包含 memory 与 temp 的目录)', + 'sys.memoryImportPrompt': '请输入 GenericAgent 根目录的完整路径(包含 memory 与 temp 的目录,而非 memory 文件夹本身):', + 'st.starting': '启动中…', 'st.stopping': '停止中…', 'st.online': '在线', 'st.offline': '离线', 'st.error': '错误', 'st.running': '运行', 'st.abnormal': '异常', 'st.missingDeps': '待安装', + 'act.configure': '配置', 'act.logs': '日志', 'act.restart': '重启', 'act.stop': '停止', 'act.start': '启动', 'act.exit': '退出', 'act.installDeps': '安装依赖', 'act.installing': '安装中…', + 'act.copy': '复制', 'act.copied': '已复制', 'act.copyTex': 'TeX', 'act.send': '发送', + 'sys.depsInstalled': '依赖安装完成', 'err.installDeps': '安装失败', + 'proc.imbotWechat': 'imbot · 微信', 'proc.imbotDing': 'imbot · 钉钉', 'proc.scheduler': '定时任务调度', 'proc.conductor': '指挥家', + 'cm.scheduling': '调度中', 'cm.running': '执行中', 'cm.idleSt': '空闲', + 'cm.master': '已派 3 子任务', 'cm.w1': '子任务:抓取数据', 'cm.w2': '子任务:复核结果', 'cm.sub': '等待派单', + 'tok.total': '累计', 'tok.cost': '缓存率', 'tok.today': '今日', 'tok.tabAll': '聊天', 'tok.tabConductor': '指挥家', 'tok.condTotal': '指挥家累计', 'tok.condCurrent': '指挥家本次', 'tok.condTip': '指挥家消耗不计入聊天累计', 'tok.condOffline': '指挥家服务离线', 'tok.disclaimer': '不同 API 网站的计费价格可能会有差异,请以实际网站为准。', + 'tok.colSession': '会话', 'tok.colIn': '输入', 'tok.colOut': '输出', 'tok.colCacheW': '缓存写入', 'tok.colCache': '缓存读取', 'tok.colCost': '成本', + 'tok.from': '从', 'tok.to': '到', 'tok.reset': '重置', 'tok.noData': '暂无记录', 'tok.deleted': '此会话已删除', + 'tok.pricingUnknown': '⚠ 此模型计费规则尚未明确,按默认估算', + 'tok.priceInput': '输入: $', 'tok.priceOutput': '输出: $', + 'tok.priceCacheW': '缓存写入: $', 'tok.priceCacheR': '缓存读取: $', + 'presetPrompt.goal': '进入 Goal 模式:读 L3 goal mode SOP,自主达成我接下来描述的目标。', + 'presetPrompt.plan': '进入 Plan 模式:先读 memory/plan_sop.md,按其中「探索→规划→执行→验证」流程,等我接下来描述要做的任务。', + 'presetPrompt.autonomous': '🤖 进入自主行动模式:阅读 memory/autonomous_operation_sop.md,按 SOP 选取或规划任务,独立执行并产出报告。', + 'presetPrompt.hive': '启动 Goal Hive 模式:按 hive SOP 拉起多个 worker 协同完成我接下来的目标。', + 'presetPrompt.review': '进入监察者模式:对刚才的产出严格挑刺、逐项复核并报告问题。', + 'presetPrompt.findwork': '按照自主行动的规划部分,充分分析我的情况,给我生成一批 TODO,务必让我感兴趣。', + 'presetPrompt.mine': '抓取本周的 git 提交并写一份周报。', + 'ask.banner': 'GA 等你回答', + 'ask.replyHint': '在下方输入框回复', + 'ask.placeholderOpen': '在此输入你的回答… (Enter 发送)', + }, + en: { + 'app.title': 'GenericAgent Desktop', + 'brand.sub': 'Desktop terminal', + 'nav.chat': 'Chat', 'nav.services': 'Services', 'nav.channels': 'Channels', 'nav.status': 'Status', + 'nav.collab': 'Conductor', 'nav.token': 'Usage', + 'foot.settings': 'Settings', 'foot.ver': 'GenericAgent · Desktop', + 'chat.startTitle': 'Start a conversation', 'chat.startSub': 'Type a message, or pick a preset', + 'preset.butler.t': 'Conductor', 'preset.butler.d': 'Auto-decompose complex tasks; just check progress and briefings', + 'preset.plan.t': 'Plan mode', 'preset.plan.d': 'Load Plan SOP — explore→plan→execute→verify', + 'preset.goal.t': 'Goal mode', 'preset.goal.d': 'Set a goal, run autonomously', + 'preset.autonomous.t': 'Autonomous mode', 'preset.autonomous.d': 'Plan/execute tasks per SOP and produce reports (same as reflect/autonomous.py)', + 'preset.hive.t': 'Hive', 'preset.hive.d': 'Multi-worker collaboration', + 'preset.review.t': 'Deep review', 'preset.review.d': 'Strict quality check', + 'preset.findwork.t': 'Find me work', 'preset.findwork.d': 'Analyze my context and suggest a batch of interesting TODOs', + 'preset.mine.t': 'My · Weekly', 'preset.mine.d': 'Custom: weekly report from commits', + 'preset.add.t': 'Custom', 'preset.add.d': 'Save any prompt as a function', + 'composer.placeholder': 'What can GA do for you?', + 'search.placeholder': 'Search chats…', 'conv.new': 'New chat', + 'ctx.pin': 'Pin', 'ctx.unpin': 'Unpin', 'ctx.rename': 'Rename', 'ctx.del': 'Delete', + 'common.close': 'Close', 'common.more': 'More', 'common.optional': 'Optional', 'common.save': 'Save', + 'modal.preset': 'Presets', 'modal.addModel': 'Add model', 'modal.editModel': 'Edit model', 'modal.settings': 'Settings', + 'modal.customPreset': 'Custom preset', + 'modal.editCustomPreset': 'Edit task', + 'customPreset.titlePh': 'Title, e.g. "Weekly report"', + 'customPreset.promptPh': 'Prompt body — sent as the message when clicked', + 'customPreset.empty': 'Title and Prompt cannot be empty', + 'customPreset.removeTitle': 'Delete', + 'customPreset.editTitle': 'Edit', + 'builtinPreset.restoreBtn': 'Restore defaults', + 'set.appearance': 'Appearance', 'set.plainUi': 'Plain', 'set.fontSize': 'Chat font size', 'set.lang': 'Language', 'set.model': 'Model', 'set.addModel': 'Add model', 'set.features': 'Features', 'set.importMykey': 'Import model config (mykey.py)', 'set.exportMykey': 'Export current model config', 'set.importMemory': 'Import existing memory & sessions (select GenericAgent root)', 'set.gaSource': 'Connect to a separate GenericAgent source (use desktop as a shell)', 'set.gaSourceClear': 'Disconnect, use the bundled version', 'set.gaSourceCurrent': 'Connected to', 'set.serviceManager': 'Service manager', + 'shortcut.askConfirm': 'Create a desktop shortcut for GenericAgent?', + 'appearance.light': 'Light', 'appearance.dark': 'Dark', + 'set.noModels': 'No models yet — add one below', + 'lang.zh': '简体中文', 'lang.en': 'English', + 'model.name': 'Note', 'model.namePh': 'Shown in the model list', + 'model.apikey': 'API Key', 'model.apikeyPh': 'sk-...', 'model.apikeyKeep': 'Leave blank to keep the current key', + 'model.apibase': 'API base URL', 'model.apibasePh': 'https://.../v1/messages', + 'model.protocol': 'Protocol', 'model.protocolPick': 'Select…', 'model.protocolOai': 'OpenAI-compatible (chat/completions)', 'model.protocolClaude': 'Anthropic (Claude /v1/messages)', + 'model.stream': 'Response', 'model.streamOn': 'Stream', 'model.streamOff': 'Non-stream', + 'model.model': 'Model', 'model.modelPh': 'model parameter name', + 'model.modelHint': 'Must match the model field in your provider docs exactly', + 'model.retries': 'Retries (×)', 'model.connTimeout': 'Connect (s)', 'model.readTimeout': 'Read (s)', + 'model.save': 'Save', 'common.cancel': 'Cancel', 'common.confirm': 'Confirm', 'common.edit': 'Edit', 'common.delete': 'Delete', + 'pq.title': 'Quick connect a model', 'pq.sub': 'Add your API key to get started', 'pq.toggle': 'Expand / collapse', + 'pq.deepseekDesc': 'Official API · OpenAI-compatible', 'pq.qwenDesc': 'Tongyi Qwen · Aliyun Bailian', + 'guide.step1': 'Open the link, sign in, then create & copy your API key', + 'guide.step2': 'Paste the key into the “API Key” field below', + 'guide.step3': 'Click Save — then pick it from the model list', + 'guide.prefillTip': 'API base, protocol and model are pre-filled — edit if needed', + 'guide.getKey': 'Get your {name} API key', 'guide.copy': 'Copy link', 'guide.copied': 'Link copied', + 'err.modelSave': 'Save failed', 'err.modelSwitch': 'Failed to switch model', 'err.modelRequired': 'Model, API Key and base URL are required', + 'err.modelDelete': 'Delete failed', 'err.modelDeleteLast': 'At least one model is required', + 'confirm.modelDelete': 'Delete this model profile?', + 'model.aggregation': 'Channel group (auto failover)', 'model.aggregationShort': 'Channel group', 'model.aggregationDesc': 'Tries in order, switches to the next on failure', + 'model.emptyMixin': 'No models added yet', + 'model.addToMixin': 'Add to channel', 'model.inMixin': 'In channel', 'model.removeFromMixin': 'Remove from channel', 'model.alreadyInMixin': 'Already in the channel', 'model.dragReorder': 'Drag to reorder', + 'err.mixinFailed': 'Operation failed', + 'page.services.title': 'Services', 'page.services.sub': 'IM channels and background processes — view, start/stop, logs', + 'page.channels.title': 'Channels', 'page.channels.sub': 'Background IM processes: list, start/stop, logs (hub.pyw style)', + 'page.status.title': 'Status', 'page.status.sub': 'Background processes/services managed by hub.pyw', + 'page.collab.title': 'Conductor', 'page.collab.sub': 'Describe a goal — split, delegate, and follow up', + 'collab.progressTitle': 'Progress', + 'collab.progressEmpty': 'No tasks running yet. After you describe a goal to Conductor, split tasks will appear here.', + 'collab.placeholder': 'Describe the goal you want to accomplish', + 'collab.guideTitle': 'Tell Conductor what you want done', + 'collab.guideWhen': 'Best for multi-step goals that take a while. For everyday chat and quick questions, use Chat in the sidebar.', + 'collab.guideStep1t': 'Describe your goal', + 'collab.guideStep1d': 'Write what you want done in the chat box and send it to Conductor', + 'collab.guideStep2t': 'Auto breakdown', + 'collab.guideStep2d': 'Conductor breaks down, assigns, monitors, and coordinates', + 'collab.guideStep3t': 'Summary', + 'collab.guideStep3d': 'Conductor delivers a briefing based on execution status', + 'collab.guideStep4t': 'Adjust anytime', + 'collab.guideStep4d': 'Add requirements or details anytime — Conductor handles them', + 'collab.chipProgress': 'How is it going?', + 'collab.chipPause': 'Pause current tasks', + 'collab.chipSummary': 'Summarize progress so far', + 'collab.showProgressTitle': 'View task progress', + 'collab.statRunning': 'Running', + 'collab.statDone': 'Done', + 'collab.plusMenu': 'More actions', + 'collab.switchMode': 'Switch mode', + 'collab.typing': 'Conductor is working', + 'collab.offline': 'Cannot reach the service. Make sure the backend is running.', + 'collab.retry': 'Retry', + 'collab.reconnect': 'Disconnected — reconnecting… Your last progress is kept.', + 'collab.reconnectIn': 'Retry in {n}s', + 'collab.stRunning': 'Running', 'collab.stReported': 'Reported', 'collab.stPaused': 'Paused', + 'collab.stFailed': 'Issue', 'collab.stTerminated': 'Ended', + 'collab.summaryRunning': 'Working…', 'collab.summaryWait': 'Awaiting report', + 'collab.taskFallback': 'Task {n}', + 'collab.timeJust': 'just now', + 'collab.timeSec': '{n}s ago', + 'collab.timeMin': '{n}m ago', + 'collab.timeHr': '{n}h ago', + 'collab.timeDay': '{n}d ago', + 'page.token.title': 'Usage', 'page.token.sub': 'Per-session and total usage & cache rate', + 'status.connecting': 'Connecting…', 'status.ready': 'Service online', 'status.running': 'Working…', + 'status.disconnected': 'Service offline', 'status.stopped': 'Stopped', 'status.idle': 'Standby', + 'conv.emptyList': 'No chats yet — click “+ New chat”', 'conv.defaultTitle': 'New chat', + 'err.bridge': 'Service not responding', 'err.newSession': 'Failed to create session', 'err.poll': 'Polling failed', 'err.stop': 'Stop failed', + 'err.interruptTimeout': 'Timed out waiting for the previous reply to stop — try again', + 'sys.interruptPrev.hint': 'Previous reply stopped — processing new message', + 'chat.interrupting': 'Stopping previous reply…', + 'chat.sessionLoading': 'Loading conversation…', + 'sys.stopRequested': 'Stop requested', + 'slash.help': 'Commands:\n/new new chat /clear clear /stop stop /settings settings', + 'slash.unknown': 'Unknown command', + 'upload.hint': 'Upload file: pick / drag / paste', + 'upload.button': 'Upload file', + 'upload.tooLarge': 'File too large or limit reached', 'upload.empty': 'Skipped empty file', + 'upload.failed': 'Upload failed', + 'err.charLimit': 'Character limit reached ({n}), text will be truncated on send', 'err.charLimitReached': 'Character limit reached ({n})', 'err.numMax': 'Cannot exceed {n}', + 'file.openFailed': 'Cannot open file', + 'file.kindGeneric': 'File', + 'file.kindDoc': 'Document', + 'file.kindSheet': 'Spreadsheet', + 'file.kindSlide': 'Slides', + 'file.kindCode': 'Code', + 'file.kindArchive': 'Archive', + 'file.kindAudio': 'Audio', + 'file.kindVideo': 'Video', + 'upload.removeTitle': 'Remove', + 'upload.dropHint': 'Drop to upload files', + 'lightbox.closeTitle': 'Close', + 'fold.thinking': 'Thinking', 'fold.tool': 'Tool call', 'fold.toolResult': 'Tool result', 'fold.llm': 'LLM Running', 'fold.turn': 'Turn {n}', + 'plan.header': 'Plan ({done}/{total})', 'plan.complete': '✓ Plan complete ({n}/{n})', + 'plan.running': 'Running plan', 'plan.completeTitle': 'Plan complete', + 'plan.placeholder': 'Plan mode activated', 'plan.waiting': 'waiting for {path} …', 'plan.overflow': '+{n} more', + 'plan.current': 'Now', 'plan.fold': 'Fold', 'plan.collapse': 'Collapse', 'plan.expand': 'Expand', 'plan.details': 'Details', 'plan.dismiss': 'Hide', + 'plan.capsuleRunning': 'Running', 'plan.capsuleComplete': 'Done', + 'timing.elapsed': 'Elapsed {t}', + 'model.auto': 'Auto', + 'model.menuLabel': 'Select model', + 'chip.plan': 'Plan', + 'chip.auto': 'Auto', + 'ch.wechat': 'WeChat', 'ch.wecom': 'WeCom', 'ch.lark': 'Lark', 'ch.dingtalk': 'DingTalk', + 'ch.qq': 'QQ', 'ch.telegram': 'Telegram', 'ch.discord': 'Discord', + 'ch.loading': 'Loading…', 'ch.empty': 'No IM process scripts found', + 'ch.logEmpty': 'No log output yet', + 'err.channelLoad': 'Failed to load', 'err.channelStart': 'Start failed', 'err.channelStop': 'Stop failed', + 'err.mykeyImport': 'Failed to import model config', + 'err.mykeyExport': 'Failed to export model config', + 'err.channelNotConfigured': 'Configure this platform in mykey.py first', + 'sys.channelStarted': 'Started', 'sys.channelStopped': 'Stopped', + 'modal.channelLogs': 'Process logs', + 'modal.mykeyConfig': 'mykey.py', + 'sys.configSaved': 'Configuration saved', + 'sys.mykeyImported': 'Model config imported', + 'sys.mykeyExported': 'Model config exported', + 'sys.memoryImported': 'Memory imported', + 'err.memoryImport': 'Failed to import memory', + 'sys.memoryImportBackup': 'Previous memory backed up to', + 'sys.memorySessions': 'sessions', + 'sys.gaSourcePickTitle': 'Select the GenericAgent source root to connect to (contains agentmain.py)', + 'sys.gaSourceSwitching': 'Switching and restarting the backend...', + 'sys.gaSourceSet': 'Connected to the external GenericAgent', + 'sys.gaSourceCleared': 'Disconnected, restored the bundled version', + 'err.gaSourceSet': 'Failed to connect', + 'err.gaSourceDesktopOnly': 'This feature is only available in the desktop app', + 'sys.memoryPickTitle': 'Select the GenericAgent root directory (the folder containing memory and temp)', + 'sys.memoryImportPrompt': 'Enter the full path of the GenericAgent root directory (the folder containing memory and temp, not the memory folder itself):', + 'st.starting': 'Starting…', 'st.stopping': 'Stopping…', 'st.online': 'Online', 'st.offline': 'Offline', 'st.error': 'Error', 'st.running': 'Running', 'st.abnormal': 'Error', 'st.missingDeps': 'Needs Setup', + 'act.configure': 'Configure', 'act.logs': 'Logs', 'act.restart': 'Restart', 'act.stop': 'Stop', 'act.start': 'Start', 'act.exit': 'Exit', 'act.installDeps': 'Install', 'act.installing': 'Installing…', + 'act.copy': 'Copy', 'act.copied': 'Copied', 'act.copyTex': 'TeX', 'act.send': 'Send', + 'sys.depsInstalled': 'Dependencies installed', 'err.installDeps': 'Install failed', + 'proc.imbotWechat': 'imbot · WeChat', 'proc.imbotDing': 'imbot · DingTalk', 'proc.scheduler': 'Scheduler', 'proc.conductor': 'Conductor', + 'cm.scheduling': 'Scheduling', 'cm.running': 'Running', 'cm.idleSt': 'Idle', + 'cm.master': 'Dispatched 3 subtasks', 'cm.w1': 'Subtask: fetch data', 'cm.w2': 'Subtask: review results', 'cm.sub': 'Waiting for tasks', + 'tok.total': 'Total', 'tok.cost': 'Cache rate', 'tok.today': 'Today', 'tok.tabAll': 'Chat', 'tok.tabConductor': 'Conductor', 'tok.condTotal': 'Conductor Total', 'tok.condCurrent': 'Conductor Current', 'tok.condTip': 'Conductor usage is not included in chat totals', 'tok.condOffline': 'Service offline', 'tok.disclaimer': 'Pricing may vary by API provider. Please refer to the actual website.', + 'tok.colSession': 'Session', 'tok.colIn': 'Input', 'tok.colOut': 'Output', 'tok.colCacheW': 'Cache write', 'tok.colCache': 'Cache read', 'tok.colCost': 'Cost', + 'tok.from': 'From', 'tok.to': 'To', 'tok.reset': 'Reset', 'tok.noData': 'No records', 'tok.deleted': 'Session deleted', + 'tok.pricingUnknown': '⚠ Pricing not confirmed, using defaults', + 'tok.priceInput': 'Input: $', 'tok.priceOutput': 'Output: $', + 'tok.priceCacheW': 'Cache write: $', 'tok.priceCacheR': 'Cache read: $', + 'presetPrompt.goal': 'Enter Goal mode: read the L3 goal-mode SOP and autonomously achieve the goal I describe next.', + 'presetPrompt.plan': 'Enter Plan mode: first read memory/plan_sop.md, follow its explore→plan→execute→verify flow, and wait for the task I describe next.', + 'presetPrompt.autonomous': '🤖 Enter autonomous mode: read memory/autonomous_operation_sop.md, follow the SOP to pick or plan a task, execute independently, and produce a report.', + 'presetPrompt.hive': 'Start Goal Hive mode: per the hive SOP, spawn multiple workers to collaboratively achieve the goal I describe next.', + 'presetPrompt.review': 'Enter reviewer mode: strictly scrutinize the previous output, review item by item and report issues.', + 'presetPrompt.findwork': 'Following the autonomous planning section, analyze my situation thoroughly and generate a batch of TODOs that genuinely interest me.', + 'presetPrompt.mine': 'Collect this week\'s git commits and write a weekly report.', + 'ask.banner': 'GA is waiting for your answer', + 'ask.replyHint': 'Reply in the input below', + 'ask.placeholderOpen': 'Type your answer here… (Enter to send)', + }, +}; + + const BOOT_I18N = { + zh: { + 'loading.starting_app': '正在启动 GenericAgent...', + 'loading.start': '首次运行:正在准备运行环境...', + 'loading.venv': '正在创建运行环境...', + 'loading.deps': '正在安装依赖...', + 'loading.done': '依赖安装完成', + 'loading.starting': '正在启动服务...', + 'setup.title': 'GenericAgent - 设置', + 'setup.heading': '需要设置', + 'setup.backend_failed': '后端未能启动,请配置下方路径。', + 'setup.prepare_error': '安装失败,错误详情:', + 'setup.py_label': 'Python 解释器路径', + 'setup.py_placeholder': 'python / 解释器路径', + 'setup.py_hint': '例如 C:/Python312/python.exe 或 ~/miniconda3/envs/myenv/bin/python', + 'setup.project_label': '项目目录', + 'setup.project_placeholder': 'GenericAgent 文件夹路径', + 'setup.project_hint': '包含 frontends/desktop_bridge.py 的文件夹', + 'setup.start': '启动', + 'setup.err_py': '请输入 Python 路径', + 'setup.err_project': '请输入项目目录', + 'setup.starting_bridge': '正在启动 bridge...', + 'setup.connected': '已连接,正在打开主窗口...', + 'setup.failed': '启动失败:{error}', + 'setup.limit': '已达字符数上限({n})' + }, + en: { + 'loading.starting_app': 'Starting GenericAgent...', + 'loading.start': 'First run: preparing the runtime...', + 'loading.venv': 'Creating the runtime environment...', + 'loading.deps': 'Installing dependencies...', + 'loading.done': 'Dependencies installed', + 'loading.starting': 'Starting services...', + 'setup.title': 'GenericAgent - Setup', + 'setup.heading': 'Setup Required', + 'setup.backend_failed': 'The backend could not start. Please configure the paths below.', + 'setup.prepare_error': 'Setup failed. Details:', + 'setup.py_label': 'Python interpreter path', + 'setup.py_placeholder': 'python / path to interpreter', + 'setup.py_hint': 'e.g. C:/Python312/python.exe or ~/miniconda3/envs/myenv/bin/python', + 'setup.project_label': 'Project directory', + 'setup.project_placeholder': 'path to GenericAgent folder', + 'setup.project_hint': 'The folder containing frontends/desktop_bridge.py', + 'setup.start': 'Start', + 'setup.err_py': 'Please enter Python path', + 'setup.err_project': 'Please enter project directory', + 'setup.starting_bridge': 'Starting bridge...', + 'setup.connected': 'Connected. Opening the main window...', + 'setup.failed': 'Failed: {error}', + 'setup.limit': 'Character limit reached ({n})' + } +}; + + for (const [code, messages] of Object.entries(BOOT_I18N)) { + I18N[code] = Object.assign(I18N[code] || {}, messages); + } + + const LANGS = Object.freeze(Object.keys(I18N)); + + function normalizeLang(code) { + return LANGS.includes(code) ? code : 'zh'; + } + + function browserLang() { + return (navigator.language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en'; + } + + function bootLang() { + try { + const saved = localStorage.getItem('ga_lang'); + if (LANGS.includes(saved)) return saved; + } catch (_) {} + return browserLang(); + } + + function tFor(lang, key, vars) { + const active = normalizeLang(lang); + let text = (I18N[active] && I18N[active][key]) || (I18N.zh && I18N.zh[key]) || (I18N.en && I18N.en[key]) || key; + if (vars) { + for (const [name, value] of Object.entries(vars)) { + text = text.replaceAll(`{${name}}`, String(value)); + } + } + return text; + } + + function apply(root, activeLang) { + const lang = normalizeLang(activeLang || bootLang()); + const scope = root || document; + if (scope === document) document.documentElement.lang = lang === 'en' ? 'en' : 'zh-CN'; + scope.querySelectorAll('[data-i18n]').forEach(el => { + el.textContent = tFor(lang, el.dataset.i18n); + }); + scope.querySelectorAll('[data-i18n-ph]').forEach(el => { + el.setAttribute('data-ph', tFor(lang, el.dataset.i18nPh)); + }); + scope.querySelectorAll('[data-i18n-placeholder]').forEach(el => { + el.setAttribute('placeholder', tFor(lang, el.dataset.i18nPlaceholder)); + }); + scope.querySelectorAll('[data-i18n-title]').forEach(el => { + el.setAttribute('title', tFor(lang, el.dataset.i18nTitle)); + el.setAttribute('aria-label', tFor(lang, el.dataset.i18nTitle)); + }); + } + + window.GA_I18N = { + dict: I18N, + languages: LANGS, + t: tFor, + apply, + bootLang, + browserLang, + }; + + window.GA_BOOT_I18N = { + get lang() { return bootLang(); }, + t(key, vars) { return tFor(bootLang(), key, vars); }, + apply(root) { return apply(root || document, bootLang()); }, + }; +})(); diff --git a/frontends/desktop/public/phosphor-icons.js b/frontends/desktop/public/phosphor-icons.js new file mode 100644 index 000000000..101df095b --- /dev/null +++ b/frontends/desktop/public/phosphor-icons.js @@ -0,0 +1,101 @@ +(() => { + const PATHS = { + chatTeardropText: + 'M172,112a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h68A8,8,0,0,1,172,112Zm-8,24H96a8,8,0,0,0,0,16h68a8,8,0,0,0,0-16Zm68-12A100.11,100.11,0,0,1,132,224H48a16,16,0,0,1-16-16V124a100,100,0,0,1,200,0Zm-16,0a84,84,0,0,0-168,0v84h84A84.09,84.09,0,0,0,216,124Z', + broadcast: + 'M128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Zm73.71,7.14a80,80,0,0,1-14.08,22.2,8,8,0,0,1-11.92-10.67,63.95,63.95,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67,80.08,80.08,0,0,1,14.08,84.47ZM69,103.09a64,64,0,0,0,11.26,67.58,8,8,0,0,1-11.92,10.67,79.93,79.93,0,0,1,0-106.67A8,8,0,1,1,80.29,85.34,63.77,63.77,0,0,0,69,103.09ZM248,128a119.58,119.58,0,0,1-34.29,84,8,8,0,1,1-11.42-11.2,103.9,103.9,0,0,0,0-145.56A8,8,0,1,1,213.71,44,119.58,119.58,0,0,1,248,128ZM53.71,200.78A8,8,0,1,1,42.29,212a119.87,119.87,0,0,1,0-168,8,8,0,1,1,11.42,11.2,103.9,103.9,0,0,0,0,145.56Z', + chartBar: + 'M224,200h-8V40a8,8,0,0,0-8-8H152a8,8,0,0,0-8,8V80H96a8,8,0,0,0-8,8v40H48a8,8,0,0,0-8,8v64H32a8,8,0,0,0,0,16H224a8,8,0,0,0,0-16ZM160,48h40V200H160ZM104,96h40V200H104ZM56,144H88v56H56Z', + usersThree: + 'M244.8,150.4a8,8,0,0,1-11.2-1.6A51.6,51.6,0,0,0,192,128a8,8,0,0,1-7.37-4.89,8,8,0,0,1,0-6.22A8,8,0,0,1,192,112a24,24,0,1,0-23.24-30,8,8,0,1,1-15.5-4A40,40,0,1,1,219,117.51a67.94,67.94,0,0,1,27.43,21.68A8,8,0,0,1,244.8,150.4ZM190.92,212a8,8,0,1,1-13.84,8,57,57,0,0,0-98.16,0,8,8,0,1,1-13.84-8,72.06,72.06,0,0,1,33.74-29.92,48,48,0,1,1,58.36,0A72.06,72.06,0,0,1,190.92,212ZM128,176a32,32,0,1,0-32-32A32,32,0,0,0,128,176ZM72,120a8,8,0,0,0-8-8A24,24,0,1,1,87.24,82a8,8,0,1,0,15.5-4A40,40,0,1,0,37,117.51,67.94,67.94,0,0,0,9.6,139.19a8,8,0,1,0,12.8,9.61A51.6,51.6,0,0,1,64,128,8,8,0,0,0,72,120Z', + coins: + 'M184,89.57V84c0-25.08-37.83-44-88-44S8,58.92,8,84v40c0,20.89,26.25,37.49,64,42.46V172c0,25.08,37.83,44,88,44s88-18.92,88-44V132C248,111.3,222.58,94.68,184,89.57ZM232,132c0,13.22-30.79,28-72,28-3.73,0-7.43-.13-11.08-.37C170.49,151.77,184,139,184,124V105.74C213.87,110.19,232,122.27,232,132ZM72,150.25V126.46A183.74,183.74,0,0,0,96,128a183.74,183.74,0,0,0,24-1.54v23.79A163,163,0,0,1,96,152,163,163,0,0,1,72,150.25Zm96-40.32V124c0,8.39-12.41,17.4-32,22.87V123.5C148.91,120.37,159.84,115.71,168,109.93ZM96,56c41.21,0,72,14.78,72,28s-30.79,28-72,28S24,97.22,24,84,54.79,56,96,56ZM24,124V109.93c8.16,5.78,19.09,10.44,32,13.57v23.37C36.41,141.4,24,132.39,24,124Zm64,48v-4.17c2.63.1,5.29.17,8,.17,3.88,0,7.67-.13,11.39-.35A121.92,121.92,0,0,0,120,171.41v23.46C100.41,189.4,88,180.39,88,172Zm48,26.25V174.4a179.48,179.48,0,0,0,24,1.6,183.74,183.74,0,0,0,24-1.54v23.79a165.45,165.45,0,0,1-48,0Zm64-3.38V171.5c12.91-3.13,23.84-7.79,32-13.57V172C232,180.39,219.59,189.4,200,194.87Z', + sidebarSimple: + 'M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM40,56H80V200H40ZM216,200H96V56H216V200Z', + pencilSimple: + 'M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z', + magnifyingGlass: + 'M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z', + books: + 'M231.65,194.55,198.46,36.75a16,16,0,0,0-19-12.39L132.65,34.42a16.08,16.08,0,0,0-12.3,19l33.19,157.8A16,16,0,0,0,169.16,224a16.25,16.25,0,0,0,3.38-.36l46.81-10.06A16.09,16.09,0,0,0,231.65,194.55ZM136,50.15c0-.06,0-.09,0-.09l46.8-10,3.33,15.87L139.33,66Zm6.62,31.47,46.82-10.05,3.34,15.9L146,97.53Zm6.64,31.57,46.82-10.06,13.3,63.24-46.82,10.06ZM216,197.94l-46.8,10-3.33-15.87L212.67,182,216,197.85C216,197.91,216,197.94,216,197.94ZM104,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V48A16,16,0,0,0,104,32ZM56,48h48V64H56Zm0,32h48v96H56Zm48,128H56V192h48v16Z', + gridFour: + 'M200,40H56A16,16,0,0,0,40,56V200a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,80H136V56h64ZM120,56v64H56V56ZM56,136h64v64H56Zm144,64H136V136h64v64Z', + robot: + 'M200,48H136V16a8,8,0,0,0-16,0V48H56A32,32,0,0,0,24,80V192a32,32,0,0,0,32,32H200a32,32,0,0,0,32-32V80A32,32,0,0,0,200,48Zm16,144a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V80A16,16,0,0,1,56,64H200a16,16,0,0,1,16,16Zm-52-56H92a28,28,0,0,0,0,56h72a28,28,0,0,0,0-56Zm-24,16v24H116V152ZM80,164a12,12,0,0,1,12-12h8v24H92A12,12,0,0,1,80,164Zm84,12h-8V152h8a12,12,0,0,1,0,24ZM72,108a12,12,0,1,1,12,12A12,12,0,0,1,72,108Zm88,0a12,12,0,1,1,12,12A12,12,0,0,1,160,108Z', + dotsThree: + 'M140,128a12,12,0,1,1-12-12A12,12,0,0,1,140,128Zm56-12a12,12,0,1,0,12,12A12,12,0,0,0,196,116ZM60,116a12,12,0,1,0,12,12A12,12,0,0,0,60,116Z', + folderSimplePlus: + 'M216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72Zm0,128H40V64H93.33L123.2,86.4A8,8,0,0,0,128,88h88Zm-56-56a8,8,0,0,1-8,8H136v16a8,8,0,0,1-16,0V152H104a8,8,0,0,1,0-16h16V120a8,8,0,0,1,16,0v16h16A8,8,0,0,1,160,144Z', + folderSimple: + 'M216,72H130.67L102.93,51.2a16.12,16.12,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V200a16,16,0,0,0,16,16H216.89A15.13,15.13,0,0,0,232,200.89V88A16,16,0,0,0,216,72Zm0,128H40V64H93.33L123.2,86.4A8,8,0,0,0,128,88h88Z', + bracketsCurly: + 'M43.18,128a29.78,29.78,0,0,1,8,10.26c4.8,9.9,4.8,22,4.8,33.74,0,24.31,1,36,24,36a8,8,0,0,1,0,16c-17.48,0-29.32-6.14-35.2-18.26-4.8-9.9-4.8-22-4.8-33.74,0-24.31-1-36-24-36a8,8,0,0,1,0-16c23,0,24-11.69,24-36,0-11.72,0-23.84,4.8-33.74C50.68,38.14,62.52,32,80,32a8,8,0,0,1,0,16C57,48,56,59.69,56,84c0,11.72,0,23.84-4.8,33.74A29.78,29.78,0,0,1,43.18,128ZM240,120c-23,0-24-11.69-24-36,0-11.72,0-23.84-4.8-33.74C205.32,38.14,193.48,32,176,32a8,8,0,0,0,0,16c23,0,24,11.69,24,36,0,11.72,0,23.84,4.8,33.74a29.78,29.78,0,0,0,8,10.26,29.78,29.78,0,0,0-8,10.26c-4.8,9.9-4.8,22-4.8,33.74,0,24.31-1,36-24,36a8,8,0,0,0,0,16c17.48,0,29.32-6.14,35.2-18.26,4.8-9.9,4.8-22,4.8-33.74,0-24.31,1-36,24-36a8,8,0,0,0,0-16Z', + gear: + 'M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm88-29.84q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.21,107.21,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.71,107.71,0,0,0-26.25-10.87,8,8,0,0,0-7.06,1.49L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.21,107.21,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06Zm-16.1-6.5a73.93,73.93,0,0,1,0,8.68,8,8,0,0,0,1.74,5.48l14.19,17.73a91.57,91.57,0,0,1-6.23,15L187,173.11a8,8,0,0,0-5.1,2.64,74.11,74.11,0,0,1-6.14,6.14,8,8,0,0,0-2.64,5.1l-2.51,22.58a91.32,91.32,0,0,1-15,6.23l-17.74-14.19a8,8,0,0,0-5-1.75h-.48a73.93,73.93,0,0,1-8.68,0,8,8,0,0,0-5.48,1.74L100.45,215.8a91.57,91.57,0,0,1-15-6.23L82.89,187a8,8,0,0,0-2.64-5.1,74.11,74.11,0,0,1-6.14-6.14,8,8,0,0,0-5.1-2.64L46.43,170.6a91.32,91.32,0,0,1-6.23-15l14.19-17.74a8,8,0,0,0,1.74-5.48,73.93,73.93,0,0,1,0-8.68,8,8,0,0,0-1.74-5.48L40.2,100.45a91.57,91.57,0,0,1,6.23-15L69,82.89a8,8,0,0,0,5.1-2.64,74.11,74.11,0,0,1,6.14-6.14A8,8,0,0,0,82.89,69L85.4,46.43a91.32,91.32,0,0,1,15-6.23l17.74,14.19a8,8,0,0,0,5.48,1.74,73.93,73.93,0,0,1,8.68,0,8,8,0,0,0,5.48-1.74L155.55,40.2a91.57,91.57,0,0,1,15,6.23L173.11,69a8,8,0,0,0,2.64,5.1,74.11,74.11,0,0,1,6.14,6.14,8,8,0,0,0,5.1,2.64l22.58,2.51a91.32,91.32,0,0,1,6.23,15l-14.19,17.74A8,8,0,0,0,199.87,123.66Z', + caretLeft: + 'M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z', + caretDown: + 'M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z', + caretRight: + 'M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z', + paperPlaneTilt: + 'M227.32,28.68a16,16,0,0,0-15.66-4.08l-.15,0L19.57,82.84a16,16,0,0,0-2.49,29.8L102,154l41.3,84.87A15.86,15.86,0,0,0,157.74,248q.69,0,1.38-.06a15.88,15.88,0,0,0,14-11.51l58.2-191.94c0-.05,0-.1,0-.15A16,16,0,0,0,227.32,28.68ZM157.83,231.85l-.05.14,0-.07-40.06-82.3,48-48a8,8,0,0,0-11.31-11.31l-48,48L24.08,98.25l-.07,0,.14,0L216,40Z', + paperclip: + 'M209.66,122.34a8,8,0,0,1,0,11.32l-82.05,82a56,56,0,0,1-79.2-79.21L147.67,35.73a40,40,0,1,1,56.61,56.55L105,193A24,24,0,1,1,71,159L154.3,74.38A8,8,0,1,1,165.7,85.6L82.39,170.31a8,8,0,1,0,11.27,11.36L192.93,81A24,24,0,1,0,159,47L59.76,147.68a40,40,0,1,0,56.53,56.62l82.06-82A8,8,0,0,1,209.66,122.34Z', + lightning: + 'M215.79,118.17a8,8,0,0,0-5-5.66L153.18,90.9l14.66-73.33a8,8,0,0,0-13.69-7l-112,120a8,8,0,0,0,3,13l57.63,21.61L88.16,238.43a8,8,0,0,0,13.69,7l112-120A8,8,0,0,0,215.79,118.17ZM109.37,214l10.47-52.38a8,8,0,0,0-5-9.06L62,132.71l84.62-90.66L136.16,94.43a8,8,0,0,0,5,9.06l52.8,19.8Z', + pushPinSimple: + 'M216,168h-9.29L185.54,48H192a8,8,0,0,0,0-16H64a8,8,0,0,0,0,16h6.46L49.29,168H40a8,8,0,0,0,0,16h80v56a8,8,0,0,0,16,0V184h80a8,8,0,0,0,0-16ZM86.71,48h82.58l21.17,120H65.54Z', + trash: + 'M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z', + x: + 'M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z', + dotsThreeVertical: + 'M140,128a12,12,0,1,1-12-12A12,12,0,0,1,140,128Zm0-56a12,12,0,1,1-12-12A12,12,0,0,1,140,72Zm0,112a12,12,0,1,1-12-12A12,12,0,0,1,140,184Z', + plus: + 'M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z', + copy: + 'M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z', + check: + 'M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z', + crosshair: + 'M232,120h-8.34A96.14,96.14,0,0,0,136,32.34V24a8,8,0,0,0-16,0v8.34A96.14,96.14,0,0,0,32.34,120H24a8,8,0,0,0,0,16h8.34A96.14,96.14,0,0,0,120,223.66V232a8,8,0,0,0,16,0v-8.34A96.14,96.14,0,0,0,223.66,136H232a8,8,0,0,0,0-16Zm-96,87.6V200a8,8,0,0,0-16,0v7.6A80.15,80.15,0,0,1,48.4,136H56a8,8,0,0,0,0-16H48.4A80.15,80.15,0,0,1,120,48.4V56a8,8,0,0,0,16,0V48.4A80.15,80.15,0,0,1,207.6,120H200a8,8,0,0,0,0,16h7.6A80.15,80.15,0,0,1,136,207.6ZM128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Z', + compass: + 'M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216ZM172.42,72.84l-64,32a8.05,8.05,0,0,0-3.58,3.58l-32,64A8,8,0,0,0,80,184a8.1,8.1,0,0,0,3.58-.84l64-32a8.05,8.05,0,0,0,3.58-3.58l32-64a8,8,0,0,0-10.74-10.74ZM138,138,97.89,158.11,118,118l40.15-20.07Z', + listChecks: + 'M224,128a8,8,0,0,1-8,8H128a8,8,0,0,1,0-16h88A8,8,0,0,1,224,128ZM128,72h88a8,8,0,0,0,0-16H128a8,8,0,0,0,0,16Zm88,112H128a8,8,0,0,0,0,16h88a8,8,0,0,0,0-16ZM82.34,42.34,56,68.69,45.66,58.34A8,8,0,0,0,34.34,69.66l16,16a8,8,0,0,0,11.32,0l32-32A8,8,0,0,0,82.34,42.34Zm0,64L56,132.69,45.66,122.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32,0l32-32a8,8,0,0,0-11.32-11.32Zm0,64L56,196.69,45.66,186.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32,0l32-32a8,8,0,0,0-11.32-11.32Z', + star: + 'M239.18,97.26A16.38,16.38,0,0,0,224.92,86l-59-4.76L143.14,26.15a16.36,16.36,0,0,0-30.27,0L90.11,81.23,31.08,86a16.46,16.46,0,0,0-9.37,28.86l45,38.83L53,211.75a16.38,16.38,0,0,0,24.5,17.82L128,198.49l50.53,31.08A16.4,16.4,0,0,0,203,211.75l-13.76-58.07,45-38.83A16.43,16.43,0,0,0,239.18,97.26Zm-15.34,5.47-48.7,42a8,8,0,0,0-2.56,7.91l14.88,62.8a.37.37,0,0,1-.17.48c-.18.14-.23.11-.38,0l-54.72-33.65a8,8,0,0,0-8.38,0L69.09,215.94c-.15.09-.19.12-.38,0a.37.37,0,0,1-.17-.48l14.88-62.8a8,8,0,0,0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16A8,8,0,0,0,103,91.86l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153,91.86a8,8,0,0,0,6.75,4.92l63.92,5.16c.15,0,.24,0,.33.29S224,102.63,223.84,102.73Z', + fileText: + 'M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Zm-32-80a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,136Zm0,32a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,168Z', + gitFork: + 'M224,64a32,32,0,1,0-40,31v17a8,8,0,0,1-8,8H80a8,8,0,0,1-8-8V95a32,32,0,1,0-16,0v17a24,24,0,0,0,24,24h40v25a32,32,0,1,0,16,0V136h40a24,24,0,0,0,24-24V95A32.06,32.06,0,0,0,224,64ZM48,64A16,16,0,1,1,64,80,16,16,0,0,1,48,64Zm96,128a16,16,0,1,1-16-16A16,16,0,0,1,144,192ZM192,80a16,16,0,1,1,16-16A16,16,0,0,1,192,80Z', + hexagon: + 'M223.68,66.15,135.68,18h0a15.88,15.88,0,0,0-15.36,0l-88,48.17a16,16,0,0,0-8.32,14v95.64a16,16,0,0,0,8.32,14l88,48.17a15.88,15.88,0,0,0,15.36,0l88-48.17a16,16,0,0,0,8.32-14V80.18A16,16,0,0,0,223.68,66.15ZM216,175.82,128,224,40,175.82V80.18L128,32h0l88,48.17Z', + }; + + function gaIcon(name, className = '') { + const d = PATHS[name]; + if (!d) return ''; + const cls = className ? ` class="${className}"` : ''; + return `