pax_global_header00006660000000000000000000000064151655114400014514gustar00rootroot0000000000000052 comment=dbde8122a36999e8d920e0979e25818fdb38dbf0 lemonade-sdk-lemonade-dbde812/000077500000000000000000000000001516551144000163345ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.clangd000066400000000000000000000001521516551144000175630ustar00rootroot00000000000000CompileFlags: CompilationDatabase: build Add: [-Wall, -Wextra, -Wpedantic] Index: Background: Build lemonade-sdk-lemonade-dbde812/.devcontainer/000077500000000000000000000000001516551144000210735ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.devcontainer/Dockerfile000066400000000000000000000015041516551144000230650ustar00rootroot00000000000000FROM mcr.microsoft.com/devcontainers/cpp:1-debian-12 ARG REINSTALL_CMAKE_VERSION_FROM_SOURCE="3.22.2" COPY .devcontainer/reinstall-cmake.sh /tmp/ RUN if [ "${REINSTALL_CMAKE_VERSION_FROM_SOURCE}" != "none" ]; then \ chmod +x /tmp/reinstall-cmake.sh && /tmp/reinstall-cmake.sh ${REINSTALL_CMAKE_VERSION_FROM_SOURCE}; \ fi \ && rm -f /tmp/reinstall-cmake.sh # Install Dependencies RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install --no-install-recommends \ python3 \ python3-pip \ python3-dev \ python3-venv \ python3-setuptools \ build-essential \ libdrm-dev \ git # Setup Venv ENV VIRTUAL_ENV=/opt/venv RUN python3 -m venv $VIRTUAL_ENV ENV PATH="$VIRTUAL_ENV/bin:$PATH" WORKDIR /usr/src/app # Copy the Project Root into the container COPY . . lemonade-sdk-lemonade-dbde812/.devcontainer/devcontainer.json000066400000000000000000000022601516551144000244470ustar00rootroot00000000000000// For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/cpp { "name": "C++", "build": { "dockerfile": "Dockerfile", "context": ".." }, "features": { "ghcr.io/devcontainers/features/python:1": {} }, "customizations": { "vscode": { "extensions": [ "ms-vscode.cmake-tools", "ms-python.python", // Official Python Extension "ms-python.vscode-pylance", // (Recommended) Better IntelliSense for Python "ms-vscode.cpptools", // Official C/C++ Extension "ms-vscode.cpptools-extension-pack" // (Optional) Adds CMake & Themes ] } } // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "gcc -v", // Configure tool-specific properties. // "customizations": {}, // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" } lemonade-sdk-lemonade-dbde812/.devcontainer/reinstall-cmake.sh000066400000000000000000000034071516551144000245060ustar00rootroot00000000000000#!/usr/bin/env bash #------------------------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. #------------------------------------------------------------------------------------------------------------- # set -e CMAKE_VERSION=${1:-"none"} if [ "${CMAKE_VERSION}" = "none" ]; then echo "No CMake version specified, skipping CMake reinstallation" exit 0 fi # Cleanup temporary directory and associated files when exiting the script. cleanup() { EXIT_CODE=$? set +e if [[ -n "${TMP_DIR}" ]]; then echo "Executing cleanup of tmp files" rm -Rf "${TMP_DIR}" fi exit $EXIT_CODE } trap cleanup EXIT echo "Installing CMake..." apt-get -y purge --auto-remove cmake mkdir -p /opt/cmake architecture=$(dpkg --print-architecture) case "${architecture}" in arm64) ARCH=aarch64 ;; amd64) ARCH=x86_64 ;; *) echo "Unsupported architecture ${architecture}." exit 1 ;; esac CMAKE_BINARY_NAME="cmake-${CMAKE_VERSION}-linux-${ARCH}.sh" CMAKE_CHECKSUM_NAME="cmake-${CMAKE_VERSION}-SHA-256.txt" TMP_DIR=$(mktemp -d -t cmake-XXXXXXXXXX) echo "${TMP_DIR}" cd "${TMP_DIR}" curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_BINARY_NAME}" -O curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_CHECKSUM_NAME}" -O sha256sum -c --ignore-missing "${CMAKE_CHECKSUM_NAME}" sh "${TMP_DIR}/${CMAKE_BINARY_NAME}" --prefix=/opt/cmake --skip-license ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake ln -s /opt/cmake/bin/ctest /usr/local/bin/ctest lemonade-sdk-lemonade-dbde812/.dockerignore000066400000000000000000000004401516551144000210060ustar00rootroot00000000000000# Git / repo metadata .git .github .gitattributes .gitignore .vscode # Dev & CI tooling .devcontainer .pylintrc # Docs mkdocs.yml README.md NOTICE.md LICENSE # Tests and Examples test examples # Python packaging (not used in image) pyproject.toml setup.py **/__pycache__ # Misc *.md lemonade-sdk-lemonade-dbde812/.gitattributes000066400000000000000000000000211516551144000212200ustar00rootroot00000000000000*.py text eol=lf lemonade-sdk-lemonade-dbde812/.github/000077500000000000000000000000001516551144000176745ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/CODEOWNERS000066400000000000000000000003161516551144000212670ustar00rootroot00000000000000# Protect AI agent and assistant configuration files from accidental overwrites AGENTS.md @ramkrishna2910 @jeremyfowers CLAUDE.md @ramkrishna2910 @jeremyfowers CLAUDE.local.md @ramkrishna2910 @jeremyfowers lemonade-sdk-lemonade-dbde812/.github/ISSUE_TEMPLATE/000077500000000000000000000000001516551144000220575ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/ISSUE_TEMPLATE/bug-report.yml000066400000000000000000000075261516551144000247020ustar00rootroot00000000000000name: "๐Ÿ› Bug Report" description: Report a bug or unexpected behavior in Lemonade labels: ["bug"] body: - type: markdown attributes: value: | Thanks for taking the time to report a bug! Please fill out the details below so we can investigate. - type: dropdown id: platform attributes: label: Platform description: Which operating system are you using? options: - Windows - Linux/Ubuntu - Linux/Fedora - Linux/Arch - Linux/Other (please specify) - macOS validations: required: true - type: input id: version attributes: label: Lemonade Version description: "Run `lemonade-server --version` or check the About dialog in the app." placeholder: "e.g. 9.4.2" validations: required: true - type: input id: gpu attributes: label: GPU / APU Model description: "Which GPU or APU are you using? You can find this in `lemonade-server status` output." placeholder: "e.g. AMD Radeon RX 7900 XTX, AMD Ryzen AI 9 HX 370" validations: required: false - type: dropdown id: backend attributes: label: Component description: Which component is involved in this issue? options: - llama.cpp - stable-diffusion.cpp - whisper.cpp - Kokoro TTS - FastFlowLM / NPU - RyzenAI - Desktop App / UI - Other / Not sure validations: required: true - type: textarea id: description attributes: label: Bug Description description: A clear and concise description of what the bug is. placeholder: "Describe what went wrong..." validations: required: true - type: textarea id: steps attributes: label: Steps to Reproduce description: Step-by-step instructions to reproduce the issue. placeholder: | 1. Start lemonade-server with `lemonade-server serve` 2. Load model X with `lemonade-server run ` 3. Send a request to ... 4. See error ... validations: required: true - type: textarea id: expected attributes: label: Expected vs Actual Behavior description: What did you expect to happen, and what actually happened? placeholder: | Expected: ... Actual: ... validations: required: false - type: textarea id: logs attributes: label: Log Output description: | Please reproduce the issue with debug logging enabled and paste the relevant log output below. **How to collect logs:** 1. Start (or restart) the server with debug logging: ``` lemonade-server serve --log-level debug ``` 2. Reproduce the issue. 3. Copy the relevant log output and paste it below, or attach the log file. **Log file locations:** - **Windows:** `%LOCALAPPDATA%\lemonade\logs\` - **Linux/macOS:** `~/.local/share/lemonade/logs/` or run `lemonade-server logs` **Additional diagnostic commands (run these and paste the output):** - `lemonade-server status` โ€” shows server info, loaded models, and GPU details - `lemonade-server recipes` โ€” shows installed backends and driver status **Platform-specific tips:** - **Windows:** Check Event Viewer (Application log) if the server crashes silently. - **Linux:** If using systemd, check `journalctl -u lemonade-server` for service logs. - **macOS:** Check Console.app or run `log show --predicate 'process == "lemonade-server"' --last 5m` render: shell validations: required: false - type: textarea id: additional attributes: label: Additional Context description: Add any other context, screenshots, or related issues here. validations: required: false lemonade-sdk-lemonade-dbde812/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000004661516551144000240550ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: "๐Ÿ’ฌ Community Discord" url: https://discord.gg/5xXzkMu8Zk about: Join our Discord community for questions, discussions, and support. - name: "๐Ÿ“ง Email Us" url: mailto:lemonade@amd.com about: Reach the Lemonade team directly via email. lemonade-sdk-lemonade-dbde812/.github/ISSUE_TEMPLATE/docs-issue.yml000066400000000000000000000021161516551144000246600ustar00rootroot00000000000000name: "๐Ÿ“– Documentation Issue" description: Report missing, incorrect, or unclear documentation labels: ["documentation"] body: - type: markdown attributes: value: | Help us improve the Lemonade documentation! Let us know what's missing or could be better. - type: input id: page attributes: label: Documentation Page / Section description: "Which page or section of the docs is this about? Provide a URL or path if possible." placeholder: "e.g. https://lemonade-server.ai/docs/server/concepts/ or docs/server/README.md" validations: required: false - type: textarea id: description attributes: label: What's Missing or Wrong description: Describe the issue with the current documentation. placeholder: "The section on X doesn't explain how to..." validations: required: true - type: textarea id: suggestion attributes: label: Suggested Improvement description: If you have a suggestion for how to fix or improve the docs, please share it. validations: required: false lemonade-sdk-lemonade-dbde812/.github/ISSUE_TEMPLATE/feature-request.yml000066400000000000000000000023621516551144000257260ustar00rootroot00000000000000name: "Feature Request" description: Suggest a new feature or improvement for Lemonade labels: ["enhancement"] body: - type: markdown attributes: value: | Have an idea for how to make Lemonade better? We'd love to hear it! - type: textarea id: description attributes: label: Feature Description description: A clear and concise description of the feature you'd like. placeholder: "Describe the feature..." validations: required: true - type: textarea id: use-case attributes: label: Use Case / Motivation description: Why is this feature important? What problem does it solve or what workflow does it improve? placeholder: "I need this because..." validations: required: true - type: dropdown id: platform attributes: label: Platform Relevance description: Which platforms would benefit from this feature? options: - All platforms - Windows only - Linux only - macOS only validations: required: true - type: textarea id: additional attributes: label: Additional Context description: Add any other context, mockups, or references here. validations: required: false lemonade-sdk-lemonade-dbde812/.github/actions/000077500000000000000000000000001516551144000213345ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/build-debian-package/000077500000000000000000000000001516551144000252445ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/build-debian-package/action.yml000066400000000000000000000034551516551144000272530ustar00rootroot00000000000000name: Build Debian Package description: Install build dependencies, build Debian package, and collect artifacts inputs: package-type: description: 'Type of package to build: binary or source' required: true deb-version: description: 'Debian version (for logging)' required: true outputs: output-dir: description: 'Directory containing collected artifacts' value: ${{ steps.build.outputs.output-dir }} runs: using: composite steps: - name: Install build dependencies shell: bash run: | set -e apt-get update && apt-get build-dep . -y - name: Build Debian package id: build shell: bash run: | set -e PACKAGE_TYPE="${{ inputs.package-type }}" DEB_VERSION="${{ inputs.deb-version }}" if [ "$PACKAGE_TYPE" = "binary" ]; then echo "Building binary .deb package for version $DEB_VERSION..." dpkg-buildpackage -us -uc -b OUTPUT_DIR="build-output" mkdir -p "$OUTPUT_DIR" mv ../*.deb "$OUTPUT_DIR/" || true mv ../*.buildinfo "$OUTPUT_DIR/" || true mv ../*.changes "$OUTPUT_DIR/" || true elif [ "$PACKAGE_TYPE" = "source" ]; then echo "Building source package for version $DEB_VERSION..." dpkg-buildpackage -us -uc -S -sa OUTPUT_DIR="source-output" mkdir -p "$OUTPUT_DIR" mv ../*.dsc "$OUTPUT_DIR/" || true mv ../*.tar.* "$OUTPUT_DIR/" || true mv ../*.changes "$OUTPUT_DIR/" || true mv ../*.buildinfo "$OUTPUT_DIR/" || true else echo "ERROR: Invalid package-type '$PACKAGE_TYPE' (must be 'binary' or 'source')" exit 1 fi echo "output-dir=$OUTPUT_DIR" >> $GITHUB_OUTPUT echo "Artifacts collected in: $OUTPUT_DIR" lemonade-sdk-lemonade-dbde812/.github/actions/build-macos-dmg/000077500000000000000000000000001516551144000243005ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/build-macos-dmg/action.yml000066400000000000000000000145331516551144000263060ustar00rootroot00000000000000name: 'Build macOS .dmg Package' description: 'Build Lemonade .dmg for macOS (signed and notarized via CMake notarize target)' inputs: include-electron: description: 'Whether to include the Electron app (full build)' required: false default: 'true' skip-packaging: description: 'Skip .pkg packaging and notarization (for unsigned local builds)' required: false default: 'false' outputs: artifact-name: description: 'The name of the generated .dmg file' value: ${{ steps.build-dmg.outputs.artifact_name }} runs: using: "composite" steps: - name: Run setup.sh to configure environment shell: bash run: | set -e echo "Running setup.sh to configure build environment..." bash setup.sh echo "Build environment configured successfully!" - name: Setup Node.js if: inputs.include-electron == 'true' uses: actions/setup-node@v5 with: node-version: '20' - name: Set Electron signing identity shell: bash run: | # Tell electron-builder to use the Developer ID Application cert from keychain if [ -n "$DEVELOPER_ID_APPLICATION_IDENTITY" ]; then echo "CSC_NAME=$DEVELOPER_ID_APPLICATION_IDENTITY" >> $GITHUB_ENV # electron-builder skips signing on PR branches by default echo "CSC_FOR_PULL_REQUEST=true" >> $GITHUB_ENV echo "Electron builder will sign with: $DEVELOPER_ID_APPLICATION_IDENTITY" else echo "WARNING: No signing identity found, electron-builder will not sign" fi - name: Build C++ Server with CMake shell: bash run: | set -e echo "Building lemond and lemonade-server for macOS..." # Reconfigure if Electron app is included (setup.sh already did basic config) EXTRA_CMAKE_ARGS="" if [ "${{ inputs.skip-packaging }}" == "true" ]; then echo "No signing identity available โ€” using ad-hoc signing" EXTRA_CMAKE_ARGS="-DSIGNING_IDENTITY=- -DINSTALLER_IDENTITY=-" fi if [ "${{ inputs.include-electron }}" == "true" ]; then echo "Reconfiguring CMake with Electron app enabled..." cmake --preset default -DBUILD_ELECTRON_APP=ON $EXTRA_CMAKE_ARGS elif [ -n "$EXTRA_CMAKE_ARGS" ]; then cmake --preset default $EXTRA_CMAKE_ARGS fi # Build (BUILD_ELECTRON_APP will trigger electron build automatically) echo "Building binaries..." cmake --build --preset default # Verify binaries exist if [ ! -f "build/lemond" ]; then echo "ERROR: lemond not found!" echo "Build directory contents:" ls -lh build/ exit 1 fi if [ ! -f "build/lemonade-server" ]; then echo "ERROR: lemonade-server not found!" echo "Build directory contents:" ls -lh build/ exit 1 fi echo "Verifying binary executability..." ./build/lemond --version ./build/lemonade-server --version # Check if binaries are signed (non-fatal) echo "Checking code signatures..." codesign --verify --verbose=2 build/lemond 2>&1 || echo "WARNING: lemond not signed (expected for non-release builds)" codesign --verify --verbose=2 build/lemonade-server 2>&1 || echo "WARNING: lemonade-server not signed (expected for non-release builds)" echo "C++ build successful!" - name: Build Web App shell: bash run: | set -e echo "Building Web app..." cd src/web-app # Install dependencies echo "Installing npm dependencies..." npm install # Build the Web app echo "Building Web app..." npm run build # Verify the build output exists if [ ! -f "dist/renderer/index.html" ]; then echo "ERROR: Web app build output not found!" exit 1 fi # Copy web app output to the location expected by CMake echo "Copying web app to build/resources/web-app..." TARGET_DIR="../../build/resources/web-app" mkdir -p "$(dirname "$TARGET_DIR")" if [ -d "$TARGET_DIR" ]; then rm -rf "$TARGET_DIR" fi cp -R dist/renderer "$TARGET_DIR" # Verify the copy succeeded if [ ! -f "../../build/resources/web-app/index.html" ]; then echo "ERROR: Failed to copy web app to build directory!" exit 1 fi echo "Web app built and copied successfully!" - name: Build .pkg and notarize if: inputs.skip-packaging != 'true' id: build-dmg shell: bash run: | set -e echo "Building macOS .pkg via CMake notarize target..." echo "This will: build electron app, sign binaries, create package, notarize, and staple." if ! cmake --build --preset default --target notarize; then echo "" echo "=========================================" echo "Notarize target failed - fetching Apple notarization log..." echo "=========================================" # Find the submission ID from notarytool output and fetch the log SUBMISSION_ID=$(xcrun notarytool history --keychain-profile AC_PASSWORD 2>/dev/null | grep -m1 "id:" | awk '{print $2}' || echo "") if [ -n "$SUBMISSION_ID" ]; then echo "Fetching log for submission: $SUBMISSION_ID" xcrun notarytool log "$SUBMISSION_ID" --keychain-profile AC_PASSWORD 2>&1 || true else echo "Could not retrieve submission ID for log lookup" xcrun notarytool history --keychain-profile AC_PASSWORD 2>&1 || true fi exit 1 fi # Find the generated .pkg PKG_FILE=$(find build -maxdepth 1 -name "*.pkg" -type f | head -1) if [ -z "$PKG_FILE" ]; then echo "ERROR: No .pkg file found after build!" echo "Contents of build directory:" ls -lh build/*.pkg build/*.dmg 2>/dev/null || true exit 1 fi ARTIFACT_NAME=$(basename "$PKG_FILE") echo "Package created: $ARTIFACT_NAME (at $PKG_FILE)" echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT echo "File details:" ls -lh "$PKG_FILE" echo "Build and notarization complete!" lemonade-sdk-lemonade-dbde812/.github/actions/capture-server-logs/000077500000000000000000000000001516551144000252455ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/capture-server-logs/action.yml000066400000000000000000000067321516551144000272550ustar00rootroot00000000000000name: 'Capture Server Logs' description: 'Find, print, and upload lemonade server logs as artifacts' inputs: artifact-name: description: 'Name for the uploaded artifact' required: true runs: using: "composite" steps: - name: Capture server logs (Windows) if: always() && runner.os == 'Windows' shell: powershell run: | New-Item -ItemType Directory -Path server-logs -Force | Out-Null # Check common log locations $candidates = @( "$env:TEMP\lemonade-server.log", "$env:TEMP\lemond.log", "$env:LOCALAPPDATA\lemonade_server\lemonade-server.log" ) $found = $false foreach ($logFile in $candidates) { if (Test-Path $logFile) { Write-Host "=== Last 200 lines of $logFile ===" Get-Content $logFile -Tail 200 Copy-Item $logFile server-logs/ -ErrorAction SilentlyContinue $found = $true } } if (-not $found) { Write-Host "No server log file found" } - name: Capture server logs (Linux/macOS) if: always() && runner.os != 'Windows' shell: bash run: | mkdir -p server-logs found=false # --- macOS: launchd redirects to /var/log/lemonade/ --- if [ "$RUNNER_OS" = "macOS" ]; then for f in /var/log/lemonade/lemonade-server.out.log /var/log/lemonade/lemonade-server.err.log; do if [ -f "$f" ]; then echo "=== Last 200 lines of $f ===" tail -200 "$f" cp "$f" server-logs/ 2>/dev/null || true found=true fi done fi # --- Linux: check file-based logs --- if [ "$RUNNER_OS" = "Linux" ]; then # XDG runtime dir (e.g. /run/user/1001/lemonade/) LEMON_RUNTIME_DIR="${XDG_RUNTIME_DIR:+${XDG_RUNTIME_DIR}/lemonade}" for candidate in \ "${LEMON_RUNTIME_DIR}/lemonade-server.log" \ /tmp/lemonade-server.log \ /tmp/lemond.log; do if [ -f "$candidate" ]; then echo "=== Last 200 lines of $candidate ===" tail -200 "$candidate" cp "$candidate" server-logs/ 2>/dev/null || true found=true fi done # Systemd journal (works when server runs as lemonade-server.service) if [ "$found" = "false" ] && command -v journalctl > /dev/null 2>&1; then echo "=== Last 200 lines from journalctl for lemonade-server ===" sudo journalctl -u lemonade-server --no-pager -n 200 2>/dev/null | tee server-logs/journald-lemonade-server.log || true if [ -s server-logs/journald-lemonade-server.log ]; then found=true else rm -f server-logs/journald-lemonade-server.log fi fi fi # --- Any platform: glob for /tmp/lemonade*.log --- for f in /tmp/lemonade*.log; do if [ -f "$f" ]; then echo "--- $f ---" tail -50 "$f" cp "$f" server-logs/ 2>/dev/null || true found=true fi done 2>/dev/null || true if [ "$found" = "false" ]; then echo "No server log files found" fi - name: Upload server logs if: always() uses: actions/upload-artifact@v7 with: name: ${{ inputs.artifact-name }} path: server-logs/ retention-days: 7 if-no-files-found: ignore lemonade-sdk-lemonade-dbde812/.github/actions/cleanup-processes-linux/000077500000000000000000000000001516551144000261245ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/cleanup-processes-linux/action.yml000066400000000000000000000111561516551144000301300ustar00rootroot00000000000000name: 'Cleanup Processes (Linux)' description: 'Kill all lemonade, Python, and llama-server processes on Linux' runs: using: "composite" steps: - name: Kill zombie processes shell: bash run: | echo "========================================" echo "Cleaning up processes on Linux..." echo "========================================" # Kill processes aggressively by pattern kill_by_pattern() { local pattern=$1 local description=$2 echo "" echo "Killing $description processes..." # Find and kill in one shot with SIGKILL pkill -9 -f "$pattern" 2>/dev/null && echo " Killed processes matching: $pattern" || echo " No $description processes found" } # Kill by exact name (even more aggressive) kill_by_exact_name() { local name=$1 if pgrep -x "$name" > /dev/null 2>&1; then echo "Killing $name (exact match)..." pkill -9 -x "$name" sleep 0.5 # Verify it's dead if pgrep -x "$name" > /dev/null 2>&1; then echo " WARNING: $name still running after kill!" else echo " Successfully killed $name" fi fi } # Kill llama processes kill_by_pattern "llama-server" "llama-server" kill_by_pattern "llama-cli" "llama-cli" kill_by_pattern "llama-bench" "llama-bench" # Kill everything lemonade-related kill_by_pattern "lemonade" "lemonade" # Kill Python processes (this is aggressive - be careful!) kill_by_pattern "python" "Python" # Additional pass: kill by exact process names echo "" echo "Second pass: killing by exact name..." for proc_name in lemonade-server lemond lemonade-server-dev lemonade-tray llama-server llama-cli llama-bench; do kill_by_exact_name "$proc_name" done # Wait for processes to die sleep 2 # FINAL VERIFICATION - show what's still alive echo "" echo "=== Verification: checking for remaining processes ===" if pgrep -f "lemonade|llama" > /dev/null 2>&1; then echo "WARNING: Some processes still alive:" ps aux | grep -E "lemonade|llama" | grep -v grep | grep -v "cleanup-processes" || true # NUCLEAR OPTION: Kill them with extreme prejudice echo "Using nuclear option..." pkill -9 -f "llama" 2>/dev/null || true pkill -9 -f "lemonade" 2>/dev/null || true sleep 1 # Check again if pgrep -f "lemonade|llama" > /dev/null 2>&1; then echo "ERROR: Processes STILL alive after nuclear option!" ps aux | grep -E "lemonade|llama" | grep -v grep || true else echo "Nuclear option successful" fi else echo "All target processes terminated successfully" fi # Clean up lock files and PID files # Check both /tmp (fallback) and $XDG_RUNTIME_DIR/lemonade (when set) echo "" echo "Cleaning up lock and PID files..." rm -f /tmp/lemonade*.lock 2>/dev/null || true rm -f /tmp/lemonade*.pid 2>/dev/null || true rm -f /tmp/lemond.pid 2>/dev/null || true if [ -n "${XDG_RUNTIME_DIR:-}" ]; then rm -f "${XDG_RUNTIME_DIR}/lemonade/lemonade"*.lock 2>/dev/null || true rm -f "${XDG_RUNTIME_DIR}/lemonade/lemonade"*.pid 2>/dev/null || true fi echo "Lock and PID files cleaned." # Clean up log files echo "" echo "Cleaning up log files..." rm -f /tmp/lemonade*.log 2>/dev/null || true rm -f /tmp/lemonade-server*.log 2>/dev/null || true rm -f /tmp/lemond*.log 2>/dev/null || true if [ -n "${XDG_RUNTIME_DIR:-}" ]; then rm -f "${XDG_RUNTIME_DIR}/lemonade/lemonade"*.log 2>/dev/null || true fi echo "Log files cleaned." # Clean up installation directories echo "" echo "Cleaning up lemonade installation directories..." if [ -d "$HOME/.local/share/lemonade-server" ]; then rm -rf "$HOME/.local/share/lemonade-server" 2>/dev/null || true echo "Removed: $HOME/.local/share/lemonade-server" else echo "No lemonade-server directory found in ~/.local/share/" fi echo "" echo "========================================" echo "Process cleanup complete!" echo "========================================" lemonade-sdk-lemonade-dbde812/.github/actions/cleanup-processes-windows/000077500000000000000000000000001516551144000264575ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/cleanup-processes-windows/action.yml000066400000000000000000000220441516551144000304610ustar00rootroot00000000000000name: 'Cleanup Processes (Windows)' description: 'Kill all lemonade, Python, llama-server, and FLM processes on Windows' runs: using: "composite" steps: - name: Kill zombie processes shell: PowerShell run: | Write-Host "========================================" -ForegroundColor Cyan Write-Host "Cleaning up processes on Windows..." -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan # Function to safely kill processes by name pattern function Stop-ProcessByPattern { param( [string]$Pattern, [string]$Description ) Write-Host "`nSearching for $Description processes..." -ForegroundColor Yellow $processes = Get-Process | Where-Object { $_.ProcessName -like "*$Pattern*" } if ($processes) { Write-Host "Found $($processes.Count) $Description process(es):" -ForegroundColor Yellow $processes | ForEach-Object { Write-Host " - $($_.ProcessName) (PID: $($_.Id))" -ForegroundColor Gray } $processes | ForEach-Object { try { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue Write-Host " Killed: $($_.ProcessName) (PID: $($_.Id))" -ForegroundColor Green } catch { Write-Host " Failed to kill: $($_.ProcessName) (PID: $($_.Id)) - $_" -ForegroundColor Red } } } else { Write-Host "No $Description processes found." -ForegroundColor Gray } } # Kill llama-server processes Stop-ProcessByPattern -Pattern "llama-server" -Description "llama-server" Stop-ProcessByPattern -Pattern "llama" -Description "llama" # Kill FLM processes (Windows only) Stop-ProcessByPattern -Pattern "flm" -Description "FLM" # Kill lemonade processes (lemonade-server, lemond, lemonade-server-dev, etc.) Stop-ProcessByPattern -Pattern "lemonade" -Description "lemonade" Stop-ProcessByPattern -Pattern "lemond" -Description "lemond" # Kill Python processes Stop-ProcessByPattern -Pattern "python" -Description "Python" # Kill wscript processes (VBScript launcher used by lemonade) Stop-ProcessByPattern -Pattern "wscript" -Description "wscript" # Wait a moment for processes to fully terminate Start-Sleep -Seconds 2 # Attempt to uninstall legacy C++ NSIS-based Lemonade Server, if present Write-Host "`nChecking for legacy Lemonade Server NSIS installer..." -ForegroundColor Yellow $nsisKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Lemonade Server' if (Test-Path $nsisKey) { try { $nsisProps = Get-ItemProperty -Path $nsisKey -ErrorAction Stop $uninstallExe = $nsisProps.UninstallString if ($uninstallExe) { Write-Host " Found NSIS uninstall registry entry. UninstallString: $uninstallExe" -ForegroundColor Gray if (Test-Path $uninstallExe) { Write-Host " Running NSIS uninstaller silently..." -ForegroundColor Yellow try { & $uninstallExe /S | Out-Null Write-Host " NSIS uninstaller process invoked." -ForegroundColor Green } catch { Write-Host " NSIS uninstaller execution failed: $_" -ForegroundColor Red } } else { Write-Host " NSIS uninstall executable path is stale (file not found). Treating as already removed." -ForegroundColor Gray } } else { Write-Host " NSIS uninstall string missing in registry. Treating as stale entry." -ForegroundColor Gray } # After running (or skipping) the uninstaller, ensure the ARP key is gone so MSI is not blocked if (Test-Path $nsisKey) { Write-Host " Removing stale NSIS ARP registry key to unblock MSI install..." -ForegroundColor Yellow try { Remove-Item -Path $nsisKey -Recurse -Force -ErrorAction Stop Write-Host " NSIS ARP registry key removed." -ForegroundColor Green } catch { Write-Host " Failed to remove NSIS ARP registry key: $_" -ForegroundColor Red } } else { Write-Host " NSIS ARP registry key already removed by uninstaller." -ForegroundColor Green } } catch { Write-Host " Failed to query NSIS uninstall key: $_" -ForegroundColor Red } } else { Write-Host " No legacy C++ NSIS installer registry key detected in HKCU (already clean)" -ForegroundColor Gray } # Clean up Python user directory that can cause issues Write-Host "`nCleaning up system Python directory..." -ForegroundColor Yellow $systemPythonPath = "C:\windows\system32\config\systemprofile\AppData\Roaming\Python" if (Test-Path $systemPythonPath) { try { Remove-Item -Path $systemPythonPath -Recurse -Force -ErrorAction SilentlyContinue Write-Host " Cleaned: $systemPythonPath" -ForegroundColor Green } catch { Write-Host " Failed to clean: $systemPythonPath - $_" -ForegroundColor Red } } else { Write-Host " System Python directory not found (already clean)" -ForegroundColor Gray } # Clean up log files to avoid confusion between test runs Write-Host "`nCleaning up log files..." -ForegroundColor Yellow $logPatterns = @("lemonade*.log", "lemond*.log", "lemonade-server*.log") foreach ($pattern in $logPatterns) { $logFiles = Get-ChildItem "$env:TEMP\$pattern" -ErrorAction SilentlyContinue if ($logFiles) { Write-Host " Found $($logFiles.Count) log file(s) matching '$pattern'" -ForegroundColor Gray $logFiles | ForEach-Object { try { Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue Write-Host " Removed: $($_.Name)" -ForegroundColor Green } catch { Write-Host " Failed to remove: $($_.Name)" -ForegroundColor Red } } } } Write-Host "`n========================================" -ForegroundColor Cyan Write-Host "Process cleanup complete!" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan - name: Uninstall Lemonade Server (MSI) shell: PowerShell run: | Write-Host "`n========================================" -ForegroundColor Magenta Write-Host "Uninstalling Lemonade Server (MSI)..." -ForegroundColor Magenta Write-Host "========================================" -ForegroundColor Magenta # Uninstall by Product Code - Attempt to find it first $productName = "Lemonade Server" Write-Host "Searching for product: $productName" -ForegroundColor Cyan $product = Get-WmiObject -Class Win32_Product | Where-Object { $_.Name -eq $productName } if ($product) { Write-Host "Found installed product: $($product.Name) (Version: $($product.Version))" -ForegroundColor Green Write-Host "IdentifyingNumber (ProductCode): $($product.IdentifyingNumber)" -ForegroundColor Gray Write-Host "Uninstalling..." -ForegroundColor Yellow $proc = Start-Process -FilePath "msiexec.exe" -ArgumentList "/x $($product.IdentifyingNumber) /qn" -Wait -PassThru if ($proc.ExitCode -eq 0) { Write-Host "Uninstall successful!" -ForegroundColor Green } else { Write-Host "Uninstall failed with exit code $($proc.ExitCode)" -ForegroundColor Red } } else { Write-Host "Product '$productName' not found installed via MSI." -ForegroundColor Gray # Fallback: Check if we can uninstall via the MSI file if it exists in current directory if (Test-Path "lemonade-server-minimal.msi") { Write-Host "Found lemonade-server-minimal.msi in current directory. Attempting uninstall via file..." -ForegroundColor Yellow $proc = Start-Process -FilePath "msiexec.exe" -ArgumentList "/x lemonade-server-minimal.msi /qn" -Wait -PassThru if ($proc.ExitCode -eq 0) { Write-Host "Uninstall via MSI file successful!" -ForegroundColor Green } else { Write-Host "Uninstall via MSI file failed with exit code $($proc.ExitCode)" -ForegroundColor Red } } } lemonade-sdk-lemonade-dbde812/.github/actions/generate-release-notes/000077500000000000000000000000001516551144000256725ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/generate-release-notes/action.yml000066400000000000000000000174461516551144000277060ustar00rootroot00000000000000name: 'Generate Release Notes' description: 'Generate formatted release notes with download table and contributor info' inputs: version: description: 'The version string (e.g., 9.1.3)' required: true repo: description: 'Repository in owner/repo format' required: true github_token: description: 'GitHub token for API access' required: true outputs: release_notes_file: description: 'Path to the release notes file' value: ${{ steps.generate.outputs.release_notes_file }} runs: using: "composite" steps: - name: Generate release notes id: generate shell: bash env: GH_TOKEN: ${{ inputs.github_token }} VERSION: ${{ inputs.version }} REPO: ${{ inputs.repo }} run: | set -e TAG_NAME="v$VERSION" echo "Generating release notes for $TAG_NAME" # Check if tag exists via GitHub API (works without local git tags) TAG_EXISTS=$(gh api repos/$REPO/git/refs/tags/$TAG_NAME --jq '.ref' 2>/dev/null || echo "") # Check if this tag already has a release on GitHub RELEASE_EXISTS=$(gh api repos/$REPO/releases/tags/$TAG_NAME --jq '.tag_name' 2>/dev/null || echo "") if [ -n "$TAG_EXISTS" ]; then # Tag exists on GitHub - use it as target (covers both new and existing releases) TARGET_COMMIT="$TAG_NAME" if [ -n "$RELEASE_EXISTS" ]; then # Release already exists - find the release before this one echo "Tag $TAG_NAME exists with release, finding previous release..." PREVIOUS_TAG=$(gh api repos/$REPO/releases --jq "[.[] | select(.tag_name != \"$TAG_NAME\")] | .[0].tag_name" 2>/dev/null || echo "") else # New release - use latest release as previous echo "Tag $TAG_NAME exists on GitHub, creating new release..." PREVIOUS_TAG=$(gh api repos/$REPO/releases/latest --jq '.tag_name' 2>/dev/null || echo "") fi else # Tag doesn't exist yet - preview mode, use HEAD echo "Tag $TAG_NAME doesn't exist yet, using HEAD for preview" PREVIOUS_TAG=$(gh api repos/$REPO/releases/latest --jq '.tag_name' 2>/dev/null || echo "") TARGET_COMMIT="HEAD" fi echo "Previous release: ${PREVIOUS_TAG:-none}" echo "Target commit: $TARGET_COMMIT" # Generate auto release notes from GitHub if [ -n "$PREVIOUS_TAG" ]; then AUTO_NOTES=$(gh api repos/$REPO/releases/generate-notes \ -f tag_name="$TAG_NAME" \ -f target_commitish="$TARGET_COMMIT" \ -f previous_tag_name="$PREVIOUS_TAG" \ --jq '.body' 2>/dev/null || echo "") else AUTO_NOTES=$(gh api repos/$REPO/releases/generate-notes \ -f tag_name="$TAG_NAME" \ -f target_commitish="$TARGET_COMMIT" \ --jq '.body' 2>/dev/null || echo "") fi echo "AUTO_NOTES length: ${#AUTO_NOTES}" echo "=== AUTO_NOTES content ===" echo "$AUTO_NOTES" echo "=== end AUTO_NOTES ===" # Extract What's Changed content (just the bullet points) WHATS_CHANGED_CONTENT="" if echo "$AUTO_NOTES" | grep -q "What's Changed"; then WHATS_CHANGED_CONTENT=$(echo "$AUTO_NOTES" | sed -n "/## What's Changed/,/## New Contributors\|## Full Changelog\|^$/p" | grep "^\* " || echo "") fi # Extract New Contributors section NEW_CONTRIBUTORS="" if echo "$AUTO_NOTES" | grep -q "New Contributors"; then NEW_CONTRIBUTORS=$(echo "$AUTO_NOTES" | sed -n "/## New Contributors/,/## Full Changelog\|^$/p" | head -20) fi # Extract Full Changelog link FULL_CHANGELOG=$(echo "$AUTO_NOTES" | grep "Full Changelog" | head -1 || echo "") # Extract unique contributors from the changelog (exclude bots) CONTRIBUTORS=$(echo "$AUTO_NOTES" | grep -oE '@[a-zA-Z0-9_-]+' | grep -ivE '^@(copilot|claude)$' | sort -u | tr '\n' ' ' | sed 's/ /, /g' | sed 's/, $//' || echo "") # Build the release notes file RELEASE_NOTES_FILE="release_notes.md" { echo "## Lemonade Server" echo "" echo "| Operating System | Downloads |" echo "|------------------|-----------|" echo "| **Windows** | [lemonade.msi](https://github.com/$REPO/releases/download/$TAG_NAME/lemonade.msi) (Server + App) ยท [lemonade-server-minimal.msi](https://github.com/$REPO/releases/download/$TAG_NAME/lemonade-server-minimal.msi) (Server Only) |" echo "| **Ubuntu 24.04+** | [Launchpad PPA](https://launchpad.net/~lemonade-team/+archive/ubuntu/stable) (Server) ยท [lemonade-app-${VERSION}-x86_64.AppImage](https://github.com/$REPO/releases/download/$TAG_NAME/lemonade-app-${VERSION}-x86_64.AppImage) (Companion Desktop App) |" echo "| **Fedora 43** | [lemonade-server-${VERSION}.x86_64.rpm](https://github.com/$REPO/releases/download/$TAG_NAME/lemonade-server-${VERSION}.x86_64.rpm) (Server) ยท [lemonade-app-${VERSION}-x86_64.AppImage](https://github.com/$REPO/releases/download/$TAG_NAME/lemonade-app-${VERSION}-x86_64.AppImage) (Companion Desktop App) |" echo "| **macOS (beta)** | [Lemonade-${VERSION}-Darwin.pkg](https://github.com/$REPO/releases/download/$TAG_NAME/Lemonade-${VERSION}-Darwin.pkg) (Server + App) |" echo "" echo "> **Other platforms?** See our [Installation Options](https://lemonade-server.ai/install_options.html) for [Docker](https://lemonade-server.ai/install_options.html#docker), [Snap](https://lemonade-server.ai/install_options.html#ubuntu), [Arch](https://lemonade-server.ai/install_options.html#arch), [Fedora](https://lemonade-server.ai/install_options.html#fedora), [Debian](https://lemonade-server.ai/install_options.html#debian), and more." echo "" echo "## Embeddable Lemonade" echo "" echo "Portable binaries for bundling into your own installer. Run \`lemond ./\` as a subprocess." echo "" echo "| Platform | Download |" echo "|----------|----------|" echo "| **Ubuntu x64** | [lemonade-embeddable-${VERSION}-ubuntu-x64.tar.gz](https://github.com/$REPO/releases/download/$TAG_NAME/lemonade-embeddable-${VERSION}-ubuntu-x64.tar.gz) |" echo "| **Windows x64** | [lemonade-embeddable-${VERSION}-windows-x64.zip](https://github.com/$REPO/releases/download/$TAG_NAME/lemonade-embeddable-${VERSION}-windows-x64.zip) |" echo "" echo "---" echo "" } > "$RELEASE_NOTES_FILE" # Build What's Changed section with thank you and collapsible details if [ -n "$WHATS_CHANGED_CONTENT" ]; then { echo "## What's Changed" echo "" if [ -n "$CONTRIBUTORS" ]; then echo "Thanks $CONTRIBUTORS for your awesome contributions to this release!" echo "" fi echo "
" echo "Click to expand changelog" echo "" echo "$WHATS_CHANGED_CONTENT" echo "" echo "
" echo "" } >> "$RELEASE_NOTES_FILE" fi [ -n "$NEW_CONTRIBUTORS" ] && echo "$NEW_CONTRIBUTORS" >> "$RELEASE_NOTES_FILE" && echo "" >> "$RELEASE_NOTES_FILE" [ -n "$FULL_CHANGELOG" ] && echo "$FULL_CHANGELOG" >> "$RELEASE_NOTES_FILE" # Code signing policy note { echo "" echo "---" echo "" echo "> Windows installers are signed. Free code signing provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org). See our [Code Signing Policy](https://github.com/$REPO#code-signing-policy)." } >> "$RELEASE_NOTES_FILE" echo "release_notes_file=$RELEASE_NOTES_FILE" >> $GITHUB_OUTPUT cat "$RELEASE_NOTES_FILE" lemonade-sdk-lemonade-dbde812/.github/actions/get-version/000077500000000000000000000000001516551144000235765ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/get-version/action.yml000066400000000000000000000014521516551144000256000ustar00rootroot00000000000000name: 'Get Lemonade Version' description: 'Extract version from CMakeLists.txt - single source of truth' outputs: version: description: 'The version string (e.g., 9.0.0)' value: ${{ steps.extract.outputs.version }} runs: using: "composite" steps: - name: Extract version from CMakeLists.txt id: extract shell: bash run: | set -e # Extract version from CMakeLists.txt VERSION=$(sed -n 's/^project(lemon_cpp VERSION \([0-9]*\.[0-9]*\.[0-9]*\).*/\1/p' CMakeLists.txt) if [ -z "$VERSION" ]; then echo "ERROR: Could not extract version from CMakeLists.txt" exit 1 fi echo "Extracted version: $VERSION" echo "version=$VERSION" >> $GITHUB_OUTPUT echo "LEMONADE_VERSION=$VERSION" >> $GITHUB_ENV lemonade-sdk-lemonade-dbde812/.github/actions/install-lemonade-deb/000077500000000000000000000000001516551144000253145ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/install-lemonade-deb/action.yml000066400000000000000000000106251516551144000273200ustar00rootroot00000000000000name: 'Install Lemonade (.deb)' description: 'Downloads and extracts Lemonade .deb package, then verifies installation' inputs: version: description: 'Version string for the .deb package (e.g., 0.5.0)' required: true artifact-name: description: 'Name of the artifact to download' required: false default: 'lemonade-deb' extract-path: description: 'Path where the .deb should be extracted' required: false default: './deb-extract' download-artifact: description: 'Whether to download the artifact (set to false if already downloaded)' required: false default: 'true' outputs: bin-path: description: 'Path to the extracted binaries directory' value: ${{ steps.extract.outputs.bin-path }} runs: using: "composite" steps: - name: Download .deb package if: ${{ inputs.download-artifact == 'true' }} uses: actions/download-artifact@v7 with: name: ${{ inputs.artifact-name }} path: . - name: Extract .deb package id: extract shell: bash run: | set -e VERSION="${{ inputs.version }}" EXTRACT_PATH="${{ inputs.extract-path }}" # Try to find the .deb file - it may have ~24.04 suffix from dpkg-buildpackage DEB_FILE=$(ls lemonade-server_${VERSION}*_amd64.deb 2>/dev/null | head -n1) if [ -z "$DEB_FILE" ]; then echo "ERROR: .deb file not found matching pattern: lemonade-server_${VERSION}*_amd64.deb" ls -la *.deb 2>/dev/null || echo "No .deb files found in current directory" exit 1 fi echo "Extracting .deb package: $DEB_FILE" # Extract the .deb package mkdir -p "$EXTRACT_PATH" dpkg-deb -x "$DEB_FILE" "$EXTRACT_PATH" # Copy resources to user's local share (path_utils.cpp checks here) mkdir -p ~/.local/share/lemonade-server cp -r "$EXTRACT_PATH/usr/share/lemonade-server/"* ~/.local/share/lemonade-server/ # Make llama directory writable for backend downloads mkdir -p "$EXTRACT_PATH/usr/share/lemonade-server/llama" chmod 777 "$EXTRACT_PATH/usr/share/lemonade-server/llama" # Make binaries executable chmod +x "$EXTRACT_PATH/usr/bin/lemonade-server" chmod +x "$EXTRACT_PATH/usr/bin/lemond" chmod +x "$EXTRACT_PATH/usr/bin/lemonade" 2>/dev/null || true # Add binaries to PATH for subsequent steps BIN_PATH="$(pwd)/$EXTRACT_PATH/usr/bin" echo "$BIN_PATH" >> $GITHUB_PATH echo "bin-path=$BIN_PATH" >> $GITHUB_OUTPUT echo "Package extracted successfully to: $EXTRACT_PATH" echo "Binaries available at: $BIN_PATH" - name: Verify binaries work shell: bash run: | set -e EXTRACT_PATH="${{ inputs.extract-path }}" echo "Verifying binaries..." # Test version commands "$EXTRACT_PATH/usr/bin/lemond" --version "$EXTRACT_PATH/usr/bin/lemonade-server" --version echo "Binaries verified successfully!" - name: Start server and wait for ready shell: bash run: | # The .deb extract doesn't install a systemd service, so start manually. # lemond logs to stdout (SinkCout), so redirect to a file for # the capture-server-logs action to find. EXTRACT_PATH="${{ inputs.extract-path }}" SERVER_BIN="$EXTRACT_PATH/usr/bin/lemond" CACHE_DIR="./" LOG_FILE="/tmp/lemonade-server.log" echo "Starting lemonade server with cache dir $CACHE_DIR (logging to $LOG_FILE)..." "$SERVER_BIN" "$CACHE_DIR" > "$LOG_FILE" 2>&1 & SERVER_PID=$! echo "Waiting for server to be ready (PID $SERVER_PID)..." for i in $(seq 1 30); do if curl -sf http://127.0.0.1:13305/live > /dev/null 2>&1; then echo "Server is running and healthy" exit 0 fi # Check if process is still alive if ! kill -0 $SERVER_PID 2>/dev/null; then echo "ERROR: Server process (PID $SERVER_PID) exited prematurely" echo "=== Server log ===" cat "$LOG_FILE" 2>/dev/null || echo "(no log file)" exit 1 fi echo "Waiting for server... ($i/30)" sleep 2 done echo "ERROR: Server did not start within 60 seconds" echo "=== Server log (last 50 lines) ===" tail -50 "$LOG_FILE" 2>/dev/null || echo "(no log file)" exit 1 lemonade-sdk-lemonade-dbde812/.github/actions/install-lemonade-server-dmg/000077500000000000000000000000001516551144000266355ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/install-lemonade-server-dmg/action.yml000066400000000000000000000067271516551144000306510ustar00rootroot00000000000000name: 'Install Lemonade Server (.pkg)' description: 'Downloads and installs Lemonade Server from a .pkg, then verifies installation' inputs: version: description: 'Version string for the package (e.g., 9.3.1)' required: true artifact-name: description: 'Name of the artifact to download' required: false default: 'lemonade-macos-pkg' download-artifact: description: 'Whether to download the artifact (set to false if already downloaded)' required: false default: 'true' outputs: bin-path: description: 'Path to the installed binaries directory' value: ${{ steps.install.outputs.bin-path }} runs: using: "composite" steps: - name: Download .pkg package if: ${{ inputs.download-artifact == 'true' }} uses: actions/download-artifact@v7 with: name: ${{ inputs.artifact-name }} path: . - name: Install .pkg id: install shell: bash run: | set -e echo "Looking for .pkg file..." PKG_FILE=$(find . -maxdepth 1 -name "*.pkg" -type f | head -1) if [ -z "$PKG_FILE" ]; then echo "ERROR: No .pkg file found in current directory!" ls -la exit 1 fi echo "Installing: $PKG_FILE" sudo installer -pkg "$PKG_FILE" -target / echo "Installation complete" # Binaries are installed at /usr/local/bin BIN_PATH="/usr/local/bin" echo "$BIN_PATH" >> $GITHUB_PATH echo "bin-path=$BIN_PATH" >> $GITHUB_OUTPUT echo "Binaries at: $BIN_PATH" - name: Verify installation shell: bash run: | set -e BIN_PATH="${{ steps.install.outputs.bin-path }}" echo "Verifying installation at: $BIN_PATH" if [ -x "$BIN_PATH/lemonade-server" ]; then echo "Found: lemonade-server" "$BIN_PATH/lemonade-server" --version else echo "ERROR: lemonade-server not found at $BIN_PATH" exit 1 fi if [ -x "$BIN_PATH/lemond" ]; then echo "Found: lemond" "$BIN_PATH/lemond" --version else echo "WARNING: lemond not found at $BIN_PATH" fi echo "Installation verification complete!" - name: Wait for server to be ready shell: bash run: | BIN_PATH="${{ steps.install.outputs.bin-path }}" LOG_FILE="/tmp/lemonade-server.log" # The .pkg postflight script starts the server via launchd. # Give it a few seconds to come up before checking. echo "Waiting for server to be ready..." for i in $(seq 1 15); do if curl -sf http://localhost:13305/live > /dev/null 2>&1; then echo "Server is running and healthy (started by launchd)" exit 0 fi sleep 2 done # Launchd may not work reliably in CI (no GUI session, restricted # sandbox). Fall back to starting the server manually. echo "Server not reachable after 30s โ€” starting manually..." "$BIN_PATH/lemonade-server" serve --no-tray > "$LOG_FILE" 2>&1 & for i in $(seq 1 15); do if curl -sf http://localhost:13305/live > /dev/null 2>&1; then echo "Server is running and healthy (started manually)" exit 0 fi echo "Waiting for server... ($i/15)" sleep 2 done echo "ERROR: Server did not start within 60 seconds" cat "$LOG_FILE" 2>/dev/null || true exit 1 lemonade-sdk-lemonade-dbde812/.github/actions/install-lemonade-server-msi/000077500000000000000000000000001516551144000266565ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/install-lemonade-server-msi/action.yml000066400000000000000000000221561516551144000306640ustar00rootroot00000000000000name: 'Install Lemonade Server (MSI)' description: 'Downloads and installs Lemonade Server MSI, then verifies installation' inputs: install-path: description: 'Path where Lemonade Server should be installed' required: true artifact-name: description: 'Name of the artifact to download' required: false default: 'Lemonade_Server_MSI' runs: using: "composite" steps: - name: Download Lemonade Server Installer uses: actions/download-artifact@v7 with: name: ${{ inputs.artifact-name }} path: . - name: Install Lemonade Server shell: PowerShell run: | $installPath = "${{ inputs.install-path }}" $logPath = "$((Get-Location).Path)\lemonade-msi-install.log" Write-Host "Installing Lemonade Server to: $installPath" -ForegroundColor Cyan Write-Host "Log file will be at: $logPath" -ForegroundColor Gray if (-not (Test-Path "lemonade-server-minimal.msi")) { Write-Host "ERROR: lemonade-server-minimal.msi not found in current directory!" -ForegroundColor Red Get-ChildItem exit 1 } # Run installer silently with verbose logging $args = "/i lemonade-server-minimal.msi /qn INSTALLDIR=`"$installPath`" /L*V `"$logPath`"" $p = Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -PassThru if ($p.ExitCode -ne 0) { Write-Host "ERROR: Installation failed with exit code $($p.ExitCode)" -ForegroundColor Red if (Test-Path $logPath) { Write-Host "=== MSI Install Log ($logPath) ===" -ForegroundColor Yellow Get-Content $logPath Write-Host "=== End MSI Install Log ===" -ForegroundColor Yellow } else { Write-Host "ERROR: Log file not found at $logPath" -ForegroundColor Red } exit $p.ExitCode } Write-Host "Installation complete." -ForegroundColor Green - name: Verify Installation shell: PowerShell run: | $ErrorActionPreference = "Stop" $installPath = "${{ inputs.install-path }}" $logPath = "$((Get-Location).Path)\lemonade-msi-install.log" Write-Host "Verifying installation at: $installPath" -ForegroundColor Cyan # Function to print log on failure function Print-InstallLog { if (Test-Path $logPath) { Write-Host "=== MSI Install Log ($logPath) ===" -ForegroundColor Yellow Get-Content $logPath Write-Host "=== End MSI Install Log ===" -ForegroundColor Yellow } else { Write-Host "Log file not found at: $logPath" -ForegroundColor Red } } # Check if installation directory exists if (-not (Test-Path $installPath)) { Write-Host "ERROR: Installation directory not found at $installPath" -ForegroundColor Red Print-InstallLog exit 1 } # Check for key binaries $expectedBinaries = @( "bin\LemonadeServer.exe", "bin\lemonade-server.exe", "bin\lemonade.exe" ) $missingItems = @() foreach ($item in $expectedBinaries) { $fullPath = Join-Path $installPath $item if (-not (Test-Path $fullPath)) { $missingItems += $item } else { Write-Host "Found: $item" -ForegroundColor Green } } if ($missingItems.Count -gt 0) { Write-Host "ERROR: Missing expected files after installation:" -ForegroundColor Red foreach ($missing in $missingItems) { Write-Host " Missing: $missing" -ForegroundColor Red } Print-InstallLog exit 1 } Write-Host "Installation directory verification successful!" -ForegroundColor Green - name: Verify Server Version shell: PowerShell run: | $ErrorActionPreference = "Stop" $installPath = "${{ inputs.install-path }}" $serverExe = Join-Path $installPath "bin\lemonade-server.exe" Write-Host "Checking lemonade-server version..." -ForegroundColor Cyan try { # Capture output and exit code $process = Start-Process -FilePath $serverExe -ArgumentList "--version" -NoNewWindow -Wait -PassThru if ($process.ExitCode -ne 0) { Write-Host "ERROR: lemonade-server --version exited with code $($process.ExitCode)" -ForegroundColor Red exit 1 } # Run again to capture output for display (Start-Process -PassThru doesn't easily capture stdout to variable without redirect) # Simpler approach for display: & $serverExe --version Write-Host "Version check passed (Exit Code 0)" -ForegroundColor Green } catch { Write-Host "ERROR: Failed to run version check: $_" -ForegroundColor Red exit 1 } - name: Restart server with CI environment shell: powershell run: | # The MSI AutoLaunchLemonade custom action auto-starts LemonadeServer.exe # after install, but that process does NOT inherit CI environment variables # (LEMONADE_CI_MODE, LEMONADE_CACHE_DIR, etc.) because msiexec's custom # action creates the process in its own environment context. # # Kill the auto-started server and restart it from this CI step so it # inherits the correct environment. $installPath = "${{ inputs.install-path }}" $serverExe = Join-Path $installPath "bin\LemonadeServer.exe" # --- Kill the MSI-auto-started server and its subprocess --- Write-Host "Stopping MSI-auto-started LemonadeServer.exe and lemond.exe..." -ForegroundColor Cyan Stop-Process -Name LemonadeServer -Force -ErrorAction SilentlyContinue Stop-Process -Name lemond -Force -ErrorAction SilentlyContinue # Wait for ports to be released (13305 for LemonadeServer, 8000 for orphan lemond) $portWait = 0 while ($portWait -lt 10) { $listening = netstat -an | Select-String ":(13305|8000) .*LISTENING" if (-not $listening) { break } Start-Sleep -Seconds 1 $portWait++ } # --- Restart with CI env vars --- Write-Host "Starting LemonadeServer.exe with CI environment..." -ForegroundColor Cyan # Pass lemonade home dir if LEMONADE_CACHE_DIR is set (config.json lives there) $homeDir = $env:LEMONADE_CACHE_DIR if ($homeDir) { Start-Process -FilePath $serverExe -ArgumentList "`"$homeDir`"" -WorkingDirectory (Split-Path $serverExe) } else { Start-Process -FilePath $serverExe -WorkingDirectory (Split-Path $serverExe) } Write-Host " LemonadeServer.exe started" -ForegroundColor Green - name: Wait for server to be ready shell: powershell run: | # --- Health check loop --- Write-Host "Waiting for server to be ready..." $maxWait = 90 $waited = 0 while ($waited -lt $maxWait) { try { $response = Invoke-WebRequest -Uri "http://localhost:13305/live" -TimeoutSec 2 -UseBasicParsing -ErrorAction Stop if ($response.StatusCode -eq 200) { Write-Host "Server is running and healthy" -ForegroundColor Green exit 0 } } catch { if ($waited % 10 -eq 0) { Write-Host " Connection error: $($_.Exception.Message)" -ForegroundColor Yellow } } Start-Sleep -Seconds 2 $waited += 2 Write-Host "Waiting for server... ($waited/$maxWait)" } # --- Failure diagnostics --- Write-Host "`n=== Failure Diagnostics ===" -ForegroundColor Red Write-Host "`nProcess check:" -ForegroundColor Cyan Get-Process -Name LemonadeServer -ErrorAction SilentlyContinue | Format-Table Id, ProcessName, StartTime -AutoSize Write-Host "`nPort 13305 state:" -ForegroundColor Cyan netstat -an | Select-String ":13305 " Write-Host "`nTrying 127.0.0.1 directly:" -ForegroundColor Cyan try { $r = Invoke-WebRequest -Uri "http://127.0.0.1:13305/live" -TimeoutSec 3 -UseBasicParsing -ErrorAction Stop Write-Host " 127.0.0.1 responded: $($r.StatusCode)" -ForegroundColor Green } catch { Write-Host " 127.0.0.1 failed: $($_.Exception.Message)" -ForegroundColor Red } Write-Host "`nTrying [::1] directly:" -ForegroundColor Cyan try { $r = Invoke-WebRequest -Uri "http://[::1]:13305/live" -TimeoutSec 3 -UseBasicParsing -ErrorAction Stop Write-Host " [::1] responded: $($r.StatusCode)" -ForegroundColor Green } catch { Write-Host " [::1] failed: $($_.Exception.Message)" -ForegroundColor Red } Write-Host "`n=== Server log ===" -ForegroundColor Yellow $logFile = Join-Path $env:TEMP "lemonade-server.log" if (Test-Path $logFile) { Get-Content $logFile -Tail 50 } Write-Error "Server did not start within $maxWait seconds" exit 1 lemonade-sdk-lemonade-dbde812/.github/actions/prepare-debian-build/000077500000000000000000000000001516551144000253075ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/prepare-debian-build/action.yml000066400000000000000000000023171516551144000273120ustar00rootroot00000000000000name: Prepare Debian Build description: Extract version from git and create Debian packaging metadata from template inputs: release: description: Ubuntu version (e.g., 24.04, 25.10, 26.04) required: true codename: description: Debian codename (e.g., noble, questing, resolute) required: true outputs: version: description: Debian version (version without leading 'v') value: ${{ steps.get_version.outputs.version }} runs: using: composite steps: - name: Get version from git describe id: get_version shell: bash run: | VERSION=$(git describe --tags --always) # Strip leading 'v' for Debian version compatibility DEB_VERSION=$(echo "${VERSION}" | sed 's/^v//') echo "version=${DEB_VERSION}" >> $GITHUB_OUTPUT - name: Create Debian packaging metadata shell: bash working-directory: ${{ github.workspace }} run: | set -e mv contrib/debian . sed -e "s|@@DEB_VERSION@@|${{ steps.get_version.outputs.version }}~${{ inputs.release }}|g" \ -e "s|@@DEB_CODENAME@@|${{ inputs.codename }}|g" \ -e "s|@@DEB_DATE@@|$(date -R)|g" \ debian/changelog.in > debian/changelog lemonade-sdk-lemonade-dbde812/.github/actions/setup-macos-keychain/000077500000000000000000000000001516551144000253655ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/setup-macos-keychain/action.yml000066400000000000000000000120051516551144000273630ustar00rootroot00000000000000name: 'Setup macOS Keychain for Signing' description: 'Import Developer ID certificates and store notarization credentials using App Store Connect API key' inputs: dev-signing-key: description: 'Base64-encoded Developer ID Application certificate (.p12)' required: true inst-signing-key: description: 'Base64-encoded Developer ID Installer certificate (.p12)' required: true certificate-password: description: 'Password for the .p12 files' required: true api-key: description: 'App Store Connect API key (.p8 content, base64-encoded)' required: true api-key-id: description: 'App Store Connect API key ID' required: true api-issuer-id: description: 'App Store Connect Issuer ID' required: true runs: using: "composite" steps: - name: Import certificates and store notarization credentials shell: bash run: | set -e echo "========================================" echo "Setting up macOS Keychain for signing..." echo "========================================" # Create a temporary keychain KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db" KEYCHAIN_PASSWORD="$(openssl rand -hex 24)" echo "Creating temporary keychain..." security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" # Import Developer ID Application certificate echo "Importing Developer ID Application certificate..." DEV_CERT_PATH="$RUNNER_TEMP/dev_certificate.p12" echo "${{ inputs.dev-signing-key }}" | base64 --decode > "$DEV_CERT_PATH" security import "$DEV_CERT_PATH" \ -k "$KEYCHAIN_PATH" \ -P "${{ inputs.certificate-password }}" \ -T /usr/bin/codesign \ -T /usr/bin/productbuild \ -T /usr/bin/pkgbuild rm -f "$DEV_CERT_PATH" # Import Developer ID Installer certificate echo "Importing Developer ID Installer certificate..." INST_CERT_PATH="$RUNNER_TEMP/inst_certificate.p12" echo "${{ inputs.inst-signing-key }}" | base64 --decode > "$INST_CERT_PATH" security import "$INST_CERT_PATH" \ -k "$KEYCHAIN_PATH" \ -P "${{ inputs.certificate-password }}" \ -T /usr/bin/codesign \ -T /usr/bin/productbuild \ -T /usr/bin/pkgbuild rm -f "$INST_CERT_PATH" # Set partition list to allow codesign access without UI prompt security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" # Add temporary keychain to search list (prepend so it takes priority) security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"') # Extract certificate fingerprints for CMake echo "Extracting certificate fingerprints..." # Get Developer ID Application identity DEV_IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep "Developer ID Application" | head -1 | awk '{print $2}') if [ -z "$DEV_IDENTITY" ]; then echo "ERROR: Developer ID Application certificate not found in keychain!" security find-identity -v "$KEYCHAIN_PATH" exit 1 fi echo "Developer ID Application: $DEV_IDENTITY" # Get Developer ID Installer identity INST_IDENTITY=$(security find-identity -v "$KEYCHAIN_PATH" | grep "Developer ID Installer" | head -1 | awk '{print $2}') if [ -z "$INST_IDENTITY" ]; then echo "ERROR: Developer ID Installer certificate not found in keychain!" security find-identity -v "$KEYCHAIN_PATH" exit 1 fi echo "Developer ID Installer: $INST_IDENTITY" # Export for CMake (CMakeLists.txt reads these at lines 861 and 869) echo "DEVELOPER_ID_APPLICATION_IDENTITY=$DEV_IDENTITY" >> $GITHUB_ENV echo "DEVELOPER_ID_INSTALLER_IDENTITY=$INST_IDENTITY" >> $GITHUB_ENV # Write the App Store Connect API key to a file for notarytool echo "Setting up App Store Connect API key for notarization..." API_KEY_PATH="$RUNNER_TEMP/AuthKey_${{ inputs.api-key-id }}.p8" echo "${{ inputs.api-key }}" | base64 --decode > "$API_KEY_PATH" # Store notarization credentials in keychain profile "AC_PASSWORD" # using App Store Connect API key (recommended by Apple for CI) # This matches the CMake notarize target at CMakeLists.txt:1026 xcrun notarytool store-credentials "AC_PASSWORD" \ --key "$API_KEY_PATH" \ --key-id "${{ inputs.api-key-id }}" \ --issuer "${{ inputs.api-issuer-id }}" rm -f "$API_KEY_PATH" # Export keychain path for cleanup echo "SIGNING_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV echo "========================================" echo "Keychain setup complete!" echo "========================================" lemonade-sdk-lemonade-dbde812/.github/actions/setup-python/000077500000000000000000000000001516551144000240135ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/setup-python/action.yml000066400000000000000000000134771516551144000260270ustar00rootroot00000000000000name: 'Setup Python' description: 'Install Python on self-hosted runners. Uses full Python installer on Windows, checks system Python on Linux.' inputs: python-version: description: 'Python version to install (e.g., 3.10, 3.12)' required: false default: '3.10' outputs: python-path: description: 'Path to the Python executable' value: ${{ steps.setup-windows.outputs.python-path || steps.setup-linux.outputs.python-path }} runs: using: "composite" steps: - name: Setup Python (Windows) id: setup-windows if: runner.os == 'Windows' shell: powershell run: | $ErrorActionPreference = "Stop" $PYTHON_VERSION = "${{ inputs.python-version }}" Write-Host "Setting up Python $PYTHON_VERSION on Windows..." # Map version to download URL for full installer switch ($PYTHON_VERSION) { "3.10" { $PYTHON_FULL = "3.10.11" } "3.11" { $PYTHON_FULL = "3.11.9" } "3.12" { $PYTHON_FULL = "3.12.7" } "3.13" { $PYTHON_FULL = "3.13.0" } default { Write-Host "Unsupported Python version: $PYTHON_VERSION" exit 1 } } $PYTHON_DIR = "$env:GITHUB_WORKSPACE\.python-$PYTHON_VERSION" $PYTHON_EXE = "$PYTHON_DIR\python.exe" # Check if already installed if (Test-Path $PYTHON_EXE) { Write-Host "Python $PYTHON_VERSION already available at $PYTHON_DIR" } else { Write-Host "Downloading Python $PYTHON_FULL..." # Use nuget to install Python (more reliable than embeddable) $nugetUrl = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" $nugetPath = "$env:GITHUB_WORKSPACE\nuget.exe" Invoke-WebRequest -Uri $nugetUrl -OutFile $nugetPath -UseBasicParsing # Install Python via nuget & $nugetPath install python -Version $PYTHON_FULL -OutputDirectory "$env:GITHUB_WORKSPACE\.nuget-packages" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Find the installed Python $pythonPackageDir = Get-ChildItem "$env:GITHUB_WORKSPACE\.nuget-packages" -Directory | Where-Object { $_.Name -like "python.$PYTHON_FULL" } | Select-Object -First 1 if (-not $pythonPackageDir) { Write-Host "ERROR: Python package not found after nuget install" Get-ChildItem "$env:GITHUB_WORKSPACE\.nuget-packages" exit 1 } $pythonToolsDir = Join-Path $pythonPackageDir.FullName "tools" # Move to our expected location if (Test-Path $PYTHON_DIR) { Remove-Item -Recurse -Force $PYTHON_DIR } Move-Item $pythonToolsDir $PYTHON_DIR # Ensure pip is installed Write-Host "Ensuring pip is installed..." & $PYTHON_EXE -m ensurepip --upgrade if ($LASTEXITCODE -ne 0) { Write-Host "ensurepip failed, trying get-pip.py..." Invoke-WebRequest -Uri "https://bootstrap.pypa.io/get-pip.py" -OutFile "$PYTHON_DIR\get-pip.py" -UseBasicParsing & $PYTHON_EXE "$PYTHON_DIR\get-pip.py" } Write-Host "Python $PYTHON_FULL installed at $PYTHON_DIR" } # Add to PATH for subsequent steps "$PYTHON_DIR" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append "$PYTHON_DIR\Scripts" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append # Output the path "python-path=$PYTHON_EXE" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append # Verify & $PYTHON_EXE --version & $PYTHON_EXE -m pip --version - name: Setup Python (Linux/macOS) id: setup-linux if: runner.os != 'Windows' shell: bash run: | PYTHON_VERSION="${{ inputs.python-version }}" echo "Setting up Python $PYTHON_VERSION..." REQUIRED_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1) REQUIRED_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2) # Try python3.X first, then python3, then python PYTHON_CMD="" for cmd in "python$PYTHON_VERSION" "python3" "python"; do if command -v "$cmd" &> /dev/null; then VERSION=$("$cmd" --version 2>&1 | grep -oP '\d+\.\d+' | head -1) MAJOR=$(echo $VERSION | cut -d. -f1) MINOR=$(echo $VERSION | cut -d. -f2) if [[ "$MAJOR" == "$REQUIRED_MAJOR" && "$MINOR" == "$REQUIRED_MINOR" ]]; then PYTHON_CMD=$(command -v "$cmd") echo "Found matching Python: $PYTHON_CMD (version $VERSION)" break elif [[ "$MAJOR" == "$REQUIRED_MAJOR" && "$MINOR" -ge "$REQUIRED_MINOR" ]]; then PYTHON_CMD=$(command -v "$cmd") echo "Found compatible Python: $PYTHON_CMD (version $VERSION, requested $PYTHON_VERSION)" break fi fi done if [ -z "$PYTHON_CMD" ]; then # Try to install from deadsnakes PPA (Ubuntu) echo "Python $PYTHON_VERSION not found. Attempting to install..." if command -v apt-get &> /dev/null; then sudo add-apt-repository -y ppa:deadsnakes/ppa 2>/dev/null || true sudo apt-get update sudo apt-get install -y "python${PYTHON_VERSION}" "python${PYTHON_VERSION}-venv" || { echo "Failed to install Python $PYTHON_VERSION from deadsnakes" echo "Falling back to system python3..." PYTHON_CMD=$(command -v python3) } PYTHON_CMD=$(command -v "python${PYTHON_VERSION}") || PYTHON_CMD=$(command -v python3) else PYTHON_CMD=$(command -v python3) fi fi if [ -z "$PYTHON_CMD" ]; then echo "ERROR: Could not find or install Python $PYTHON_VERSION" exit 1 fi echo "python-path=$PYTHON_CMD" >> $GITHUB_OUTPUT "$PYTHON_CMD" --version lemonade-sdk-lemonade-dbde812/.github/actions/setup-venv/000077500000000000000000000000001516551144000234505ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/actions/setup-venv/action.yml000066400000000000000000000223031516551144000254500ustar00rootroot00000000000000name: 'Setup Virtual Environment' description: 'Create a Python virtual environment in the checkout folder for clean isolation' inputs: venv-name: description: 'Name for the virtual environment directory (will be created in workspace)' required: false default: '.venv' python-version: description: 'Python version to use (passed to setup-python action on self-hosted)' required: false default: '3.10' requirements-file: description: 'Optional requirements file to install' required: false default: '' install-package: description: 'Optional package specification to install (e.g., ".[dev]" or "package-name")' required: false default: '' extra-index-url: description: 'Optional extra index URL for pip' required: false default: '' outputs: venv-path: description: 'Full path to the venv directory' value: ${{ steps.create-venv-windows.outputs.venv-path || steps.create-venv-linux.outputs.venv-path }} python-path: description: 'Path to the Python executable in the venv' value: ${{ steps.create-venv-windows.outputs.python-path || steps.create-venv-linux.outputs.python-path }} runs: using: "composite" steps: # For GitHub-hosted runners, use actions/setup-python - name: Setup Python (GitHub-hosted) if: ${{ !contains(runner.name, 'stx') && !contains(runner.name, 'rai') && !contains(runner.name, 'selfhost') }} uses: actions/setup-python@v6 with: python-version: ${{ inputs.python-version }} # For self-hosted runners, use our custom setup - name: Setup Python (self-hosted) id: setup-python-selfhosted if: ${{ contains(runner.name, 'stx') || contains(runner.name, 'rai') || contains(runner.name, 'selfhost') }} uses: ./.github/actions/setup-python with: python-version: ${{ inputs.python-version }} # Set the Python path for use in subsequent steps - name: Set Python path (self-hosted Windows) if: ${{ (contains(runner.name, 'stx') || contains(runner.name, 'rai') || contains(runner.name, 'selfhost')) && runner.os == 'Windows' }} shell: powershell run: | $PYTHON_DIR = "$env:GITHUB_WORKSPACE\.python-${{ inputs.python-version }}" $PYTHON_EXE = "$PYTHON_DIR\python.exe" Write-Host "Using Python at: $PYTHON_EXE" "SETUP_PYTHON_PATH=$PYTHON_EXE" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - name: Set Python path (self-hosted Linux/macOS) if: ${{ (contains(runner.name, 'stx') || contains(runner.name, 'rai') || contains(runner.name, 'selfhost')) && runner.os != 'Windows' }} shell: bash run: | # Use the python that setup-python found/installed PYTHON_EXE="${{ steps.setup-python-selfhosted.outputs.python-path }}" echo "Using Python at: $PYTHON_EXE" echo "SETUP_PYTHON_PATH=$PYTHON_EXE" >> $GITHUB_ENV # Set Python path for GitHub-hosted runners (Windows) - name: Set Python path (GitHub-hosted Windows) if: ${{ !contains(runner.name, 'stx') && !contains(runner.name, 'rai') && !contains(runner.name, 'selfhost') && runner.os == 'Windows' }} shell: powershell run: | # On GitHub-hosted runners, actions/setup-python puts python in PATH $PYTHON_EXE = (Get-Command python).Source Write-Host "Using Python at: $PYTHON_EXE" "SETUP_PYTHON_PATH=$PYTHON_EXE" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append # Set Python path for GitHub-hosted runners (Linux/macOS) - name: Set Python path (GitHub-hosted Linux/macOS) if: ${{ !contains(runner.name, 'stx') && !contains(runner.name, 'rai') && !contains(runner.name, 'selfhost') && runner.os != 'Windows' }} shell: bash run: | # On GitHub-hosted runners, actions/setup-python puts python in PATH PYTHON_EXE=$(which python3 || which python) echo "Using Python at: $PYTHON_EXE" echo "SETUP_PYTHON_PATH=$PYTHON_EXE" >> $GITHUB_ENV - name: Create virtual environment (Windows) id: create-venv-windows if: runner.os == 'Windows' shell: powershell run: | $ErrorActionPreference = "Stop" $VENV_NAME = "${{ inputs.venv-name }}" $VENV_PATH = "$env:GITHUB_WORKSPACE\$VENV_NAME" $BASE_PYTHON = $env:SETUP_PYTHON_PATH Write-Host "Creating virtual environment at $VENV_PATH..." Write-Host "Using base Python: $BASE_PYTHON" # Verify we're using the correct Python & $BASE_PYTHON --version # Remove existing venv if present if (Test-Path $VENV_PATH) { Write-Host "Removing existing venv..." Remove-Item -Recurse -Force $VENV_PATH } # Create the venv using the explicit Python path & $BASE_PYTHON -m venv $VENV_PATH if ($LASTEXITCODE -ne 0) { Write-Host "venv creation failed, trying with --without-pip..." & $BASE_PYTHON -m venv $VENV_PATH --without-pip if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Install pip manually Write-Host "Installing pip manually..." Invoke-WebRequest -Uri "https://bootstrap.pypa.io/get-pip.py" -OutFile "$VENV_PATH\get-pip.py" -UseBasicParsing & "$VENV_PATH\Scripts\python.exe" "$VENV_PATH\get-pip.py" } $PYTHON_PATH = "$VENV_PATH\Scripts\python.exe" $PIP_PATH = "$VENV_PATH\Scripts\pip.exe" # PREPEND venv Scripts to PATH (write to file first, then prepend) # This ensures venv Python takes precedence over system Python "$VENV_PATH\Scripts" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append # Upgrade pip & $PYTHON_PATH -m pip install --upgrade pip "venv-path=$VENV_PATH" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append "python-path=$PYTHON_PATH" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append Write-Host "Virtual environment created successfully!" Write-Host "Venv Python location:" & $PYTHON_PATH -c "import sys; print(sys.executable)" & $PYTHON_PATH --version - name: Create virtual environment (Linux/macOS) id: create-venv-linux if: runner.os != 'Windows' shell: bash run: | VENV_NAME="${{ inputs.venv-name }}" VENV_PATH="$GITHUB_WORKSPACE/$VENV_NAME" BASE_PYTHON="$SETUP_PYTHON_PATH" echo "Creating virtual environment at $VENV_PATH..." echo "Using base Python: $BASE_PYTHON" # Verify we're using the correct Python "$BASE_PYTHON" --version # Remove existing venv if present if [ -d "$VENV_PATH" ]; then echo "Removing existing venv..." rm -rf "$VENV_PATH" fi # Create the venv using the explicit Python path "$BASE_PYTHON" -m venv "$VENV_PATH" PYTHON_PATH="$VENV_PATH/bin/python" PIP_PATH="$VENV_PATH/bin/pip" # Add bin to PATH (prepended in subsequent steps) echo "$VENV_PATH/bin" >> $GITHUB_PATH # Upgrade pip "$PYTHON_PATH" -m pip install --upgrade pip echo "venv-path=$VENV_PATH" >> $GITHUB_OUTPUT echo "python-path=$PYTHON_PATH" >> $GITHUB_OUTPUT echo "Virtual environment created successfully!" echo "Venv Python location:" "$PYTHON_PATH" -c "import sys; print(sys.executable)" "$PYTHON_PATH" --version - name: Install requirements file (Windows) if: inputs.requirements-file != '' && runner.os == 'Windows' shell: powershell run: | $VENV_PATH = "${{ steps.create-venv-windows.outputs.venv-path }}" $PIP = "$VENV_PATH\Scripts\pip.exe" Write-Host "Installing requirements from ${{ inputs.requirements-file }}..." & $PIP install -r "${{ inputs.requirements-file }}" - name: Install requirements file (Linux/macOS) if: inputs.requirements-file != '' && runner.os != 'Windows' shell: bash run: | VENV_PATH="${{ steps.create-venv-linux.outputs.venv-path }}" PIP="$VENV_PATH/bin/pip" echo "Installing requirements from ${{ inputs.requirements-file }}..." "$PIP" install -r "${{ inputs.requirements-file }}" - name: Install package (Windows) if: inputs.install-package != '' && runner.os == 'Windows' shell: powershell run: | $VENV_PATH = "${{ steps.create-venv-windows.outputs.venv-path }}" $PIP = "$VENV_PATH\Scripts\pip.exe" $EXTRA_INDEX = "" if ("${{ inputs.extra-index-url }}" -ne "") { $EXTRA_INDEX = "--extra-index-url ${{ inputs.extra-index-url }}" } Write-Host "Installing package: ${{ inputs.install-package }}..." $cmd = "$PIP install ${{ inputs.install-package }} $EXTRA_INDEX" Invoke-Expression $cmd - name: Install package (Linux/macOS) if: inputs.install-package != '' && runner.os != 'Windows' shell: bash run: | VENV_PATH="${{ steps.create-venv-linux.outputs.venv-path }}" PIP="$VENV_PATH/bin/pip" EXTRA_INDEX="" if [ -n "${{ inputs.extra-index-url }}" ]; then EXTRA_INDEX="--extra-index-url ${{ inputs.extra-index-url }}" fi echo "Installing package: ${{ inputs.install-package }}..." "$PIP" install ${{ inputs.install-package }} $EXTRA_INDEX lemonade-sdk-lemonade-dbde812/.github/dependabot.yml000066400000000000000000000007231516551144000225260ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for more information: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates # https://containers.dev/guide/dependabot version: 2 updates: - package-ecosystem: "devcontainers" directory: "/" schedule: interval: weekly lemonade-sdk-lemonade-dbde812/.github/workflows/000077500000000000000000000000001516551144000217315ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.github/workflows/build-and-push-container.yml000066400000000000000000000033311516551144000272500ustar00rootroot00000000000000name: Build and Push Container Image on: push: tags: - "v*" workflow_dispatch: inputs: tag: description: "Git tag to build (e.g. v9.1.1)" required: true add_latest: description: "Also tag this image as latest" type: boolean default: false permissions: contents: read packages: write env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }}-server jobs: build-and-push: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v5 with: fetch-depth: 0 # needed to fetch tags - name: Determine tag id: tag run: | if [ "${{ github.event_name }}" = "push" ]; then echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT else echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT git checkout "refs/tags/${{ inputs.tag }}" fi - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and push image uses: docker/build-push-action@v5 with: context: . file: Dockerfile push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} ${{ github.event_name == 'push' && format('{0}/{1}:latest', env.REGISTRY, env.IMAGE_NAME) || '' }} ${{ github.event_name == 'workflow_dispatch' && inputs.add_latest && format('{0}/{1}:latest', env.REGISTRY, env.IMAGE_NAME) || '' }} lemonade-sdk-lemonade-dbde812/.github/workflows/build-container.yml000066400000000000000000000033531516551144000255370ustar00rootroot00000000000000name: Publish Containers on: schedule: - cron: '0 2 * * *' # Nightly at 2 AM UTC workflow_dispatch: jobs: build-container: runs-on: ubuntu-latest strategy: matrix: include: - ubuntu_version: "24.04" ubuntu_ppa: "ppa:lemonade-team/bleeding-edge" - ubuntu_version: "25.10" ubuntu_ppa: "ppa:lemonade-team/bleeding-edge" - ubuntu_version: "26.04" ubuntu_ppa: "" permissions: contents: read packages: write steps: - name: Checkout code uses: actions/checkout@v5 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/lemonade-sdk/lemonade/build-environment tags: | type=raw,value=ubuntu${{ matrix.ubuntu_version }} type=raw,value=latest,enable=${{ matrix.ubuntu_version == '26.04' }} type=raw,value=nightly,enable=${{ matrix.ubuntu_version == '26.04' }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . file: .ubuntu/Dockerfile build-args: | BASE_IMAGE=ubuntu:${{ matrix.ubuntu_version }} UBUNTU_PPA=${{ matrix.ubuntu_ppa }} push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max lemonade-sdk-lemonade-dbde812/.github/workflows/claude-review.yml000066400000000000000000000025571516551144000252210ustar00rootroot00000000000000name: Claude Code Review on: pull_request_target: types: [review_requested] issue_comment: types: [created] pull_request_review_comment: types: [created] permissions: contents: read pull-requests: write issues: write id-token: write jobs: claude-review: # Only run for maintainers (OWNER, MEMBER, COLLABORATOR) # Triggers on: @claude mention in comments, or assigning claude as reviewer if: | (github.event_name == 'pull_request_target' && github.event.action == 'review_requested' && github.event.requested_reviewer.login == 'claude[bot]') || ((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && contains(github.event.comment.body, '@claude') && (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR')) runs-on: ubuntu-latest concurrency: group: claude-review-${{ github.event.pull_request.number || github.event.issue.number }} cancel-in-progress: true steps: - uses: actions/checkout@v5 with: fetch-depth: 1 - uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} lemonade-sdk-lemonade-dbde812/.github/workflows/cpp_server_build_test_release.yml000066400000000000000000001726511516551144000305560ustar00rootroot00000000000000name: C++ Server Build, Test, and Release ๐Ÿš€ on: push: branches: ["main"] tags: - v* pull_request: merge_group: workflow_dispatch: inputs: enable_signing: description: 'Enable MSI signing with SignPath (for testing)' required: false default: false type: boolean permissions: contents: write actions: read # Required for SignPath to read workflow/job details env: LEMONADE_DISABLE_SYSTEMD_JOURNAL: "1" jobs: # ======================================================================== # BUILD JOBS - Run on rai-160-sdk workers # ======================================================================== build-lemonade-server-installer: name: Build Lemonade Server Installer runs-on: windows-latest outputs: unsigned-artifact-id: ${{ steps.upload-unsigned-msi.outputs.artifact-id }} steps: - uses: actions/checkout@v5 with: clean: true fetch-depth: 0 - name: Install CMake if not available shell: PowerShell run: | # Check if CMake is already installed $cmakeInstalled = Get-Command cmake -ErrorAction SilentlyContinue if (-not $cmakeInstalled) { Write-Host "CMake not found, installing..." -ForegroundColor Yellow # Download CMake installer $cmakeVersion = "3.28.1" $cmakeUrl = "https://github.com/Kitware/CMake/releases/download/v$cmakeVersion/cmake-$cmakeVersion-windows-x86_64.msi" $cmakeInstaller = "cmake-installer.msi" Invoke-WebRequest -Uri $cmakeUrl -OutFile $cmakeInstaller -UseBasicParsing # Install CMake silently Start-Process msiexec.exe -ArgumentList "/i $cmakeInstaller /quiet /norestart" -Wait # Add CMake to PATH for this session AND future steps $cmakePath = "C:\Program Files\CMake\bin" $env:PATH = "$cmakePath;$env:PATH" # Persist to GITHUB_PATH for future steps echo $cmakePath >> $env:GITHUB_PATH # Verify installation cmake --version if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: CMake installation failed!" -ForegroundColor Red exit 1 } Write-Host "CMake installed successfully and added to PATH!" -ForegroundColor Green } else { Write-Host "CMake is already installed:" -ForegroundColor Green cmake --version } - name: Install WiX Toolset 5.0.2 (CLI) shell: PowerShell run: | $ErrorActionPreference = "Stop" Write-Host "Downloading WiX Toolset 5.0.2 CLI..." -ForegroundColor Cyan $wixUri = "https://github.com/wixtoolset/wix/releases/download/v5.0.2/wix-cli-x64.msi" $msiPath = "$env:RUNNER_TEMP\wix-cli-x64.msi" Invoke-WebRequest -UseBasicParsing -Uri $wixUri -OutFile $msiPath Write-Host "Installing WiX Toolset 5.0.2 CLI..." -ForegroundColor Cyan $p = Start-Process "msiexec.exe" -ArgumentList @("/i", "`"$msiPath`"", "/qn", "/norestart") -PassThru -Wait if ($p.ExitCode -ne 0) { Write-Host "WiX installer exited with code $($p.ExitCode)" -ForegroundColor Red exit $p.ExitCode } - name: Verify WiX installation shell: PowerShell run: | $ErrorActionPreference = "Stop" # WiX CLI MSI does not always add itself to PATH in non-interactive installs, # so we locate it explicitly and then update PATH for subsequent steps. $wixDirs = @( "C:\Program Files\WiX Toolset v5.0\bin", "C:\Program Files (x86)\WiX Toolset v5.0\bin" ) $wixExe = $null foreach ($dir in $wixDirs) { if (Test-Path (Join-Path $dir "wix.exe")) { $wixExe = Join-Path $dir "wix.exe" break } } if (-not $wixExe) { Write-Host "ERROR: wix.exe not found after installation." -ForegroundColor Red Get-ChildItem -Recurse "C:\Program Files" -Filter wix.exe -ErrorAction SilentlyContinue | Select-Object -First 20 | Format-List FullName exit 1 } $wixDir = Split-Path $wixExe -Parent # Persist wix.exe directory to PATH for all subsequent steps "$wixDir" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append Write-Host "Using WiX from: $wixExe" -ForegroundColor Green & $wixExe --version - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: '20' - name: Cache FetchContent dependencies uses: actions/cache@v5 with: path: build/_deps key: fetchcontent-windows-${{ hashFiles('CMakeLists.txt') }} restore-keys: | fetchcontent-windows- - name: Configure CMake shell: PowerShell run: | $ErrorActionPreference = "Stop" # Clean build directory (but preserve cached _deps) if (Test-Path "build") { Get-ChildItem -Path "build" -Exclude "_deps" | Remove-Item -Recurse -Force } cmake --preset windows if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Build installers shell: PowerShell run: | $ErrorActionPreference = "Stop" Write-Host "Building C++ server, Electron app, web app, and MSI installers..." -ForegroundColor Cyan # Single build command: wix_installers depends on all C++ targets, # electron-app, and web-app via CMake dependency graph cmake --build --preset windows --target wix_installers if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Verify outputs $failures = @() @( "build\Release\lemond.exe", "build\Release\lemonade-server.exe", "build\Release\LemonadeServer.exe", "build\app\win-unpacked\lemonade-app.exe", "build\resources\web-app\index.html", "lemonade-server-minimal.msi", "lemonade.msi" ) | ForEach-Object { if (-not (Test-Path $_)) { Write-Host "ERROR: $_ not found!" -ForegroundColor Red $failures += $_ } } if ($failures.Count -gt 0) { exit 1 } Write-Host "Build and packaging successful!" -ForegroundColor Green - name: Upload Lemonade Server Installers id: upload-unsigned-msi uses: actions/upload-artifact@v7 with: name: Lemonade_Server_MSI path: | lemonade-server-minimal.msi lemonade.msi retention-days: 7 sign-msi-installers: name: Sign MSI Installers with SignPath runs-on: windows-latest needs: build-lemonade-server-installer # Sign on tag pushes (releases) or when manually enabled via workflow_dispatch if: startsWith(github.ref, 'refs/tags/v') || inputs.enable_signing == true steps: - name: Sign MSI Installers with SignPath id: sign-msi uses: signpath/github-action-submit-signing-request@v2 with: api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' organization-id: '8103545b-7814-4edc-86d6-a91dc2a2291b' project-slug: 'lemonade' signing-policy-slug: 'release-signing' github-artifact-id: '${{ needs.build-lemonade-server-installer.outputs.unsigned-artifact-id }}' wait-for-completion: true wait-for-completion-timeout-in-seconds: 3600 output-artifact-directory: 'signed-msi' parameters: | version: "${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || 'test' }}" - name: Verify Signed MSI Files shell: PowerShell run: | Write-Host "Verifying signed MSI files..." -ForegroundColor Cyan if (-not (Test-Path "signed-msi\lemonade-server-minimal.msi")) { Write-Host "ERROR: Signed lemonade-server-minimal.msi not found!" -ForegroundColor Red exit 1 } if (-not (Test-Path "signed-msi\lemonade.msi")) { Write-Host "ERROR: Signed lemonade.msi not found!" -ForegroundColor Red exit 1 } Write-Host "Signed MSI files verified!" -ForegroundColor Green Get-ChildItem -Path "signed-msi" -Recurse | Format-Table Name, Length - name: Upload Signed MSI Installers uses: actions/upload-artifact@v7 with: name: Lemonade_Server_MSI_Signed path: | signed-msi/lemonade-server-minimal.msi signed-msi/lemonade.msi retention-days: 7 build-lemonade-deb: name: Build Lemonade .deb Package runs-on: ubuntu-latest outputs: version: ${{ steps.get_version.outputs.version }} container: image: ghcr.io/lemonade-sdk/lemonade/build-environment:ubuntu24.04 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v5 with: clean: true fetch-depth: 0 - name: Configure git safe directory run: git config --global --add safe.directory $(pwd) - name: Prepare Debian build id: get_version uses: ./.github/actions/prepare-debian-build with: release: '24.04' codename: 'noble' - name: Build Debian package id: build_deb uses: ./.github/actions/build-debian-package with: package-type: binary deb-version: ${{ steps.get_version.outputs.version }} - name: Upload .deb package uses: actions/upload-artifact@v7 with: name: lemonade-deb path: ${{ steps.build_deb.outputs.output-dir }}/*.deb retention-days: 7 build-lemonade-rpm: name: Build Lemonade .rpm Package runs-on: ubuntu-latest container: image: fedora:latest outputs: version: ${{ steps.get_version.outputs.version }} steps: - uses: actions/checkout@v5 with: clean: true fetch-depth: 0 - name: Install RPM packaging tools shell: bash run: | set -e dnf install -y rpm-build - name: Get version from CMakeLists.txt id: get_version uses: ./.github/actions/get-version - name: Build Linux .rpm package shell: bash run: | set -e echo "Running setup.sh to configure build environment..." bash setup.sh echo "Building lemond and lemonade-server for Fedora..." cmake --build --preset default RPM_FILE="lemonade-server-${LEMONADE_VERSION}.x86_64.rpm" cd build echo "Creating .rpm package with CPack..." cpack -G RPM -V if [ ! -f "$RPM_FILE" ]; then echo "ERROR: .rpm package not created!" echo "Contents of build directory:" ls -lR . exit 1 fi echo "Package information:" rpm -qip "$RPM_FILE" - name: Upload .rpm package uses: actions/upload-artifact@v7 with: name: lemonade-rpm path: build/lemonade-server-${{ env.LEMONADE_VERSION }}.x86_64.rpm retention-days: 7 build-lemonade-macos-dmg: name: Build Lemonade macOS .dmg (with Electron App) runs-on: macos-latest outputs: version: ${{ steps.get_version.outputs.version }} has_signing: ${{ steps.check_signing.outputs.has_signing }} steps: - uses: actions/checkout@v5 with: clean: true fetch-depth: 0 - name: Get version from CMakeLists.txt id: get_version uses: ./.github/actions/get-version - name: Check signing secrets id: check_signing shell: bash env: APP_CONNECT_KEY: ${{ secrets.MACOS_APP_CONNECT_KEY_GERAMY }} run: | if [ -n "$APP_CONNECT_KEY" ]; then echo "has_signing=true" >> $GITHUB_OUTPUT echo "Signing secrets available - will build signed .pkg" else echo "has_signing=false" >> $GITHUB_OUTPUT echo "No signing secrets - will build and test locally" fi - name: Setup macOS Keychain if: steps.check_signing.outputs.has_signing == 'true' uses: ./.github/actions/setup-macos-keychain with: dev-signing-key: ${{ secrets.MACOS_DEV_SIGNING_IDENTITY_KEY }} inst-signing-key: ${{ secrets.MACOS_INST_SIGNING_IDENTITY_KEY }} certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} api-key: ${{ secrets.MACOS_APP_CONNECT_KEY_GERAMY }} api-key-id: '3WFZZ8F948' api-issuer-id: '2e545619-8206-4d14-9ba9-ef23eff841b2' - name: Build macOS .dmg uses: ./.github/actions/build-macos-dmg with: include-electron: 'true' skip-packaging: ${{ steps.check_signing.outputs.has_signing != 'true' }} - name: Upload .pkg package if: steps.check_signing.outputs.has_signing == 'true' uses: actions/upload-artifact@v7 with: name: lemonade-macos-pkg path: build/*.pkg retention-days: 7 # ---- Unsigned local install + test path ---- - name: Install binaries locally (unsigned path) if: steps.check_signing.outputs.has_signing != 'true' shell: bash run: | set -e echo "Installing binaries locally (no signing)..." # Copy binaries to /usr/local/bin sudo cp build/lemond /usr/local/bin/ sudo cp build/lemonade-server /usr/local/bin/ sudo cp build/lemonade /usr/local/bin/ sudo chmod 755 /usr/local/bin/lemond sudo chmod 755 /usr/local/bin/lemonade-server sudo chmod 755 /usr/local/bin/lemonade # Copy resources sudo mkdir -p "/Library/Application Support/Lemonade/resources" if [ -d "build/resources" ]; then sudo cp -R build/resources/* "/Library/Application Support/Lemonade/resources/" 2>/dev/null || true fi # Run the postinst script for directory setup echo "Running post-install script..." sudo bash src/cpp/postinst-full-mac "" "/" # Verify installation echo "Verifying installation..." /usr/local/bin/lemonade-server --version /usr/local/bin/lemond --version /usr/local/bin/lemonade --version echo "Local installation complete!" - name: Verify server is running (unsigned path) if: steps.check_signing.outputs.has_signing != 'true' shell: bash run: | echo "Checking server health..." for i in $(seq 1 15); do if curl -sf http://localhost:13305/live > /dev/null 2>&1; then echo "Server is running and healthy" exit 0 fi sleep 2 done # Launchd may not start reliably in CI. Fall back to manual start. echo "Server not reachable after 30s โ€” starting manually..." /usr/local/bin/lemonade-server serve --no-tray > /tmp/lemonade-server.log 2>&1 & for i in $(seq 1 15); do if curl -sf http://localhost:13305/live > /dev/null 2>&1; then echo "Server is running and healthy (started manually)" exit 0 fi echo "Waiting for server... ($i/15)" sleep 2 done echo "ERROR: Server did not start within 60 seconds" cat /tmp/lemonade-server.log 2>/dev/null || true exit 1 - name: Setup Python and virtual environment (unsigned path) if: steps.check_signing.outputs.has_signing != 'true' uses: ./.github/actions/setup-venv with: venv-name: '.venv' python-version: '3.10' requirements-file: 'test/requirements.txt' - name: Run CLI tests (unsigned path) if: steps.check_signing.outputs.has_signing != 'true' shell: bash env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} HF_HOME: ${{ github.workspace }}/hf-cache GGML_METAL_NO_RESIDENCY: "1" run: | set -e echo "Running CLI tests..." .venv/bin/python test/server_cli.py --server-binary /usr/local/bin/lemonade-server .venv/bin/python test/server_cli2.py --server-binary /usr/local/bin/lemonade-server echo "CLI tests PASSED!" - name: Run endpoint tests (unsigned path) if: steps.check_signing.outputs.has_signing != 'true' shell: bash env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} HF_HOME: ${{ github.workspace }}/hf-cache GGML_METAL_NO_RESIDENCY: "1" run: | set -e echo "Running endpoint tests..." .venv/bin/python test/server_endpoints.py --server-binary /usr/local/bin/lemonade-server echo "Endpoint tests PASSED!" - name: Run Ollama API tests (unsigned path) if: steps.check_signing.outputs.has_signing != 'true' shell: bash env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} HF_HOME: ${{ github.workspace }}/hf-cache GGML_METAL_NO_RESIDENCY: "1" run: | set -e echo "Running Ollama API tests..." .venv/bin/python test/test_ollama.py --server-binary /usr/local/bin/lemonade-server echo "Ollama API tests PASSED!" - name: Run streaming error tests (unsigned path) if: steps.check_signing.outputs.has_signing != 'true' shell: bash env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} HF_HOME: ${{ github.workspace }}/hf-cache GGML_METAL_NO_RESIDENCY: "1" run: | set -e echo "Running streaming error termination tests..." .venv/bin/python test/server_streaming_errors.py --server-binary /usr/local/bin/lemonade-server echo "Streaming error tests PASSED!" - name: Run environment variable tests (unsigned path) if: steps.check_signing.outputs.has_signing != 'true' shell: bash env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 run: | set -e echo "Running environment variable tests..." .venv/bin/python test/server_env_vars.py --lemond-binary /usr/local/bin/lemond echo "Environment variable tests PASSED!" - name: Cleanup keychain if: always() && steps.check_signing.outputs.has_signing == 'true' shell: bash run: | if [ -n "$SIGNING_KEYCHAIN_PATH" ] && [ -f "$SIGNING_KEYCHAIN_PATH" ]; then echo "Cleaning up temporary keychain..." security delete-keychain "$SIGNING_KEYCHAIN_PATH" 2>/dev/null || true fi build-lemonade-appimage: name: Build Lemonade AppImage runs-on: ubuntu-latest outputs: version: ${{ steps.get_version.outputs.version }} steps: - uses: actions/checkout@v5 with: clean: true fetch-depth: 0 - name: Get version from CMakeLists.txt id: get_version uses: ./.github/actions/get-version - name: Run setup.sh to configure environment shell: bash run: | set -e echo "Running setup.sh to configure build environment..." bash setup.sh echo "Build environment configured successfully!" - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: '20' - name: Build AppImage shell: bash run: | set -e echo "Building Lemonade AppImage..." cmake --build --preset default --target appimage # Verify AppImage was created APPIMAGE_FILE="build/app-appimage/lemonade-app-${LEMONADE_VERSION}-x86_64.AppImage" if [ ! -f "$APPIMAGE_FILE" ]; then echo "ERROR: AppImage not created!" echo "Contents of build/app-appimage directory:" ls -lh build/app-appimage/ || echo "Directory does not exist" exit 1 fi echo "AppImage created successfully!" ls -lh "$APPIMAGE_FILE" # Verify it's executable chmod +x "$APPIMAGE_FILE" "$APPIMAGE_FILE" --appimage-help | head -5 - name: Upload AppImage artifact uses: actions/upload-artifact@v7 with: name: lemonade-appimage path: build/app-appimage/lemonade-app-${{ env.LEMONADE_VERSION }}-x86_64.AppImage retention-days: 7 build-lemonade-embeddable-linux: name: Build Embeddable Lemonade (Linux) runs-on: ubuntu-latest outputs: version: ${{ steps.get_version.outputs.version }} steps: - uses: actions/checkout@v4 with: clean: true fetch-depth: 0 - name: Get version from CMakeLists.txt id: get_version uses: ./.github/actions/get-version - name: Install minimal build dependencies run: | set -e sudo apt-get update sudo apt-get install -y cmake ninja-build g++ pkg-config libssl-dev libdrm-dev - name: Build embeddable archive shell: bash run: | set -e # Do not use setup.sh โ€” we intentionally avoid installing system # libraries so that FetchContent statically links all deps, making # the embeddable binary portable across Ubuntu versions. cmake --preset default -DBUILD_WEB_APP=OFF cmake --build --preset default --target embeddable ARCHIVE="build/lemonade-embeddable-${LEMONADE_VERSION}-ubuntu-x64.tar.gz" test -f "$ARCHIVE" echo "Archive contents:" tar tzf "$ARCHIVE" ls -lh "$ARCHIVE" - name: Upload embeddable archive uses: actions/upload-artifact@v4 with: name: lemonade-embeddable-linux path: build/lemonade-embeddable-${{ env.LEMONADE_VERSION }}-ubuntu-x64.tar.gz retention-days: 7 build-lemonade-embeddable-windows: name: Build Embeddable Lemonade (Windows) runs-on: windows-latest outputs: version: ${{ steps.get_version.outputs.version }} steps: - uses: actions/checkout@v4 with: clean: true fetch-depth: 0 - name: Install CMake if not available shell: PowerShell run: | $cmakeInstalled = Get-Command cmake -ErrorAction SilentlyContinue if (-not $cmakeInstalled) { Write-Host "CMake not found, installing..." -ForegroundColor Yellow $cmakeVersion = "3.28.1" $cmakeUrl = "https://github.com/Kitware/CMake/releases/download/v$cmakeVersion/cmake-$cmakeVersion-windows-x86_64.msi" $cmakeInstaller = "cmake-installer.msi" Invoke-WebRequest -Uri $cmakeUrl -OutFile $cmakeInstaller -UseBasicParsing Start-Process msiexec.exe -ArgumentList "/i $cmakeInstaller /quiet /norestart" -Wait $cmakePath = "C:\Program Files\CMake\bin" $env:PATH = "$cmakePath;$env:PATH" echo $cmakePath >> $env:GITHUB_PATH cmake --version if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: CMake installation failed!" -ForegroundColor Red exit 1 } Write-Host "CMake installed successfully!" -ForegroundColor Green } else { Write-Host "CMake is already installed:" -ForegroundColor Green cmake --version } - name: Get version from CMakeLists.txt id: get_version uses: ./.github/actions/get-version - name: Cache FetchContent dependencies uses: actions/cache@v4 with: path: build/_deps key: fetchcontent-windows-embeddable-${{ hashFiles('CMakeLists.txt') }} restore-keys: fetchcontent-windows-embeddable- - name: Build embeddable archive shell: PowerShell run: | $ErrorActionPreference = "Stop" cmake --preset windows -DBUILD_WEB_APP=OFF if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } cmake --build --preset windows --target embeddable if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } $archive = "build\lemonade-embeddable-$($env:LEMONADE_VERSION)-windows-x64.zip" if (-not (Test-Path $archive)) { throw "Archive not found: $archive" } Write-Host "Archive created:" -ForegroundColor Green Get-ChildItem $archive - name: Upload embeddable archive uses: actions/upload-artifact@v4 with: name: lemonade-embeddable-windows path: build/lemonade-embeddable-${{ env.LEMONADE_VERSION }}-windows-x64.zip retention-days: 7 # ======================================================================== # TEST JOBS - Inference tests on self-hosted runners # ======================================================================== test-exe-inference: name: Test .exe - ${{ matrix.name }} runs-on: ${{ matrix.runner }} needs: build-lemonade-server-installer # Skip inference tests when signing is enabled (tag pushes or manual workflow_dispatch) if: ${{ !startsWith(github.ref, 'refs/tags/') && inputs.enable_signing != true }} strategy: fail-fast: false matrix: include: - name: llamacpp script: server_llm.py extra_args: "--wrapped-server llamacpp" backends: "vulkan rocm" runner: [rai300_400, Windows] - name: ryzenai script: server_llm.py extra_args: "--wrapped-server ryzenai" backends: "cpu hybrid npu" runner: [rai300_400, Windows] - name: flm script: server_llm.py extra_args: "--wrapped-server flm" backends: "npu" runner: [rai300_400, Windows] - name: whisper script: server_whisper.py extra_args: "--wrapped-server whispercpp" backends: "cpu npu" runner: [rai300_400, Windows] - name: flm-whisper script: server_whisper.py extra_args: "--wrapped-server flm" backends: "npu" runner: [rai300_400, Windows] - name: stable-diffusion script: server_sd.py extra_args: "" backends: "cpu" runner: [rai300_400, Windows] - name: text-to-speech script: server_tts.py extra_args: "" backends: "" runner: [rai300_400, Windows] - name: stable-diffusion (stx-halo) script: server_sd.py extra_args: "" backends: "rocm" runner: [stx-halo, Windows] env: LEMONADE_CI_MODE: "True" LEMONADE_CACHE_DIR: ".\\ci-cache" PYTHONIOENCODING: utf-8 GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} steps: - uses: actions/checkout@v5 - name: Cleanup processes uses: ./.github/actions/cleanup-processes-windows - name: Set environment variables shell: PowerShell run: | $cwd = (Get-Item .\).FullName echo "HF_HOME=$cwd\hf-cache" >> $Env:GITHUB_ENV echo "LEMONADE_INSTALL_PATH=$cwd\lemonade_server_install" >> $Env:GITHUB_ENV - name: Install and Verify Lemonade Server uses: ./.github/actions/install-lemonade-server-msi with: install-path: ${{ env.LEMONADE_INSTALL_PATH }} - name: Setup Python and virtual environment uses: ./.github/actions/setup-venv with: venv-name: '.venv' python-version: '3.10' requirements-file: 'test/requirements.txt' - name: Install FLM backend for FLM wrapped-server tests if: ${{ runner.os == 'Windows' && contains(matrix.extra_args, '--wrapped-server flm') }} shell: PowerShell run: | $ErrorActionPreference = "Stop" $lemonadeExe = Join-Path $env:LEMONADE_INSTALL_PATH "bin\lemonade.exe" Write-Host "Installing FLM backend for CI inference tests..." -ForegroundColor Cyan & $lemonadeExe backends install flm:npu if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Run tests shell: PowerShell env: HF_HOME: ${{ env.HF_HOME }} run: | $ErrorActionPreference = "Stop" $venvPython = ".\.venv\Scripts\python.exe" $serverExe = Join-Path $env:LEMONADE_INSTALL_PATH "bin\lemonade-server.exe" $extraArgs = "${{ matrix.extra_args }}" -split " " | Where-Object { $_ } $backends = "${{ matrix.backends }}" -split " " | Where-Object { $_ } if ($backends.Count -eq 0) { Write-Host "Running test/${{ matrix.script }} ${{ matrix.extra_args }}" -ForegroundColor Cyan & $venvPython test/${{ matrix.script }} @extraArgs --server-binary $serverExe if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } else { foreach ($backend in $backends) { Write-Host "Running test/${{ matrix.script }} ${{ matrix.extra_args }} --backend $backend" -ForegroundColor Cyan & $venvPython test/${{ matrix.script }} @extraArgs --backend $backend --server-binary $serverExe if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } } - name: Capture and upload server logs if: always() uses: ./.github/actions/capture-server-logs with: artifact-name: server-logs-exe-${{ matrix.name }} - name: Cleanup if: always() uses: ./.github/actions/cleanup-processes-windows test-deb-inference: name: Test .deb - ${{ matrix.name }} runs-on: [rai300_400, Linux] needs: build-lemonade-deb # Skip inference tests when signing is enabled (tag pushes or manual workflow_dispatch) if: ${{ !startsWith(github.ref, 'refs/tags/') && inputs.enable_signing != true }} strategy: fail-fast: false matrix: include: - name: llamacpp script: server_llm.py extra_args: "--wrapped-server llamacpp" backends: "vulkan rocm" - name: stable-diffusion script: server_sd.py extra_args: "" backends: "cpu rocm" - name: whisper script: server_whisper.py extra_args: "--wrapped-server whispercpp" backends: "cpu vulkan" - name: flm script: server_llm.py extra_args: "--wrapped-server flm" backends: "npu" - name: text-to-speech script: server_tts.py extra_args: "" backends: "" env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} LEMONADE_VERSION: ${{ needs.build-lemonade-deb.outputs.version }} steps: - uses: actions/checkout@v5 - name: Cleanup processes uses: ./.github/actions/cleanup-processes-linux - name: Set HF_HOME environment variable run: echo "HF_HOME=$PWD/hf-cache" >> $GITHUB_ENV - name: Install FLM backend for FLM wrapped-server tests if: ${{ contains(matrix.extra_args, '--wrapped-server flm') }} run: | set -e FLM_VERSION=$(jq -r '.flm.npu' src/cpp/resources/backend_versions.json) FLM_VERSION_NUM=$(echo $FLM_VERSION | sed 's/^v//') echo "Installing FLM ${FLM_VERSION} for CI inference tests..." curl -L -o /tmp/fastflowlm.deb "https://github.com/FastFlowLM/FastFlowLM/releases/download/${FLM_VERSION}/fastflowlm_${FLM_VERSION_NUM}_ubuntu24.04_amd64.deb" mkdir -p /tmp/flm-extract dpkg-deb -x /tmp/fastflowlm.deb /tmp/flm-extract # Add extracted FLM binary to PATH (Linux uses system PATH for FLM discovery) FLM_BIN=$(find /tmp/flm-extract -name flm -type f | head -1) chmod +x "$FLM_BIN" echo "$(dirname "$FLM_BIN")" >> $GITHUB_PATH export PATH="$(dirname "$FLM_BIN"):$PATH" rm /tmp/fastflowlm.deb flm version - name: Install Lemonade (.deb) uses: ./.github/actions/install-lemonade-deb with: version: ${{ env.LEMONADE_VERSION }} - name: Setup Python and virtual environment uses: ./.github/actions/setup-venv with: venv-name: '.venv' python-version: '3.10' requirements-file: 'test/requirements.txt' - name: Run tests env: HF_HOME: ${{ env.HF_HOME }} run: | set -e if [ -z "${{ matrix.backends }}" ]; then echo "Running test/${{ matrix.script }} ${{ matrix.extra_args }}" .venv/bin/python test/${{ matrix.script }} ${{ matrix.extra_args }} --server-binary lemonade-server else for backend in ${{ matrix.backends }}; do echo "Running test/${{ matrix.script }} ${{ matrix.extra_args }} --backend $backend" .venv/bin/python test/${{ matrix.script }} ${{ matrix.extra_args }} --backend $backend --server-binary lemonade-server done fi - name: Capture and upload server logs if: always() uses: ./.github/actions/capture-server-logs with: artifact-name: server-logs-deb-${{ matrix.name }} - name: Cleanup if: always() uses: ./.github/actions/cleanup-processes-linux test-rpm-package: name: Test .rpm - Fedora runs-on: ubuntu-latest needs: build-lemonade-rpm container: image: fedora:latest env: LEMONADE_VERSION: ${{ needs.build-lemonade-rpm.outputs.version }} steps: - name: Download Lemonade .rpm Package uses: actions/download-artifact@v7 with: name: lemonade-rpm path: . - name: Install and verify Lemonade (.rpm) shell: bash run: | set -e RPM_FILE="lemonade-server-${LEMONADE_VERSION}.x86_64.rpm" if [ ! -f "$RPM_FILE" ]; then echo "ERROR: .rpm file not found: $RPM_FILE" ls -la *.rpm 2>/dev/null || echo "No .rpm files found in current directory" exit 1 fi dnf install -y shadow-utils "$RPM_FILE" echo "Installed package information:" rpm -qi lemonade-server echo "Installed file list:" rpm -ql lemonade-server | sort test -f /opt/bin/lemonade-server test -f /opt/bin/lemond /opt/bin/lemonade-server --version /opt/bin/lemond --version test-embeddable-linux: name: Test Embeddable (Linux) runs-on: ubuntu-latest needs: build-lemonade-embeddable-linux env: LEMONADE_VERSION: ${{ needs.build-lemonade-embeddable-linux.outputs.version }} steps: - name: Download embeddable archive uses: actions/download-artifact@v4 with: name: lemonade-embeddable-linux path: . - name: Validate archive structure shell: bash run: | set -e ARCHIVE="lemonade-embeddable-${LEMONADE_VERSION}-ubuntu-x64.tar.gz" test -f "$ARCHIVE" tar xzf "$ARCHIVE" DIR="lemonade-embeddable-${LEMONADE_VERSION}-ubuntu-x64" # Verify expected files exist test -f "$DIR/lemond" test -f "$DIR/lemonade" test -f "$DIR/LICENSE" test -f "$DIR/resources/server_models.json" test -f "$DIR/resources/backend_versions.json" test -f "$DIR/resources/defaults.json" echo "All expected files present" # Verify NO forbidden files FORBIDDEN=0 if ls "$DIR"/lemonade-server* 2>/dev/null; then echo "ERROR: Found lemonade-server (legacy CLI) in archive"; FORBIDDEN=1 fi if [ -d "$DIR/resources/web-app" ]; then echo "ERROR: Found web-app directory in archive"; FORBIDDEN=1 fi if ls "$DIR"/lemonade-app* 2>/dev/null; then echo "ERROR: Found electron app in archive"; FORBIDDEN=1 fi if ls "$DIR"/lemonade-tray* 2>/dev/null; then echo "ERROR: Found tray app in archive"; FORBIDDEN=1 fi if ls "$DIR"/LemonadeServer* 2>/dev/null; then echo "ERROR: Found LemonadeServer in archive"; FORBIDDEN=1 fi if [ "$FORBIDDEN" -ne 0 ]; then exit 1; fi echo "No forbidden files found" - name: Test version commands shell: bash run: | set -e DIR="lemonade-embeddable-${LEMONADE_VERSION}-ubuntu-x64" "$DIR/lemond" --version "$DIR/lemonade" --version - name: Test lemond startup and health check shell: bash run: | set -e DIR="lemonade-embeddable-${LEMONADE_VERSION}-ubuntu-x64" cd "$DIR" ./lemond ./ & LEMOND_PID=$! # Wait for server to become healthy for i in $(seq 1 15); do if curl -sf http://localhost:13305/api/v1/health > /dev/null 2>&1; then echo "Server is healthy!" break fi if [ $i -eq 15 ]; then echo "ERROR: Server failed to start within 30 seconds" kill $LEMOND_PID 2>/dev/null || true exit 1 fi sleep 2 done # Verify health endpoint responds with valid JSON HEALTH=$(curl -sf http://localhost:13305/api/v1/health) echo "Health response: $HEALTH" # Clean shutdown kill $LEMOND_PID 2>/dev/null || true wait $LEMOND_PID 2>/dev/null || true echo "Embeddable Linux smoke test PASSED!" test-embeddable-windows: name: Test Embeddable (Windows) runs-on: windows-latest needs: build-lemonade-embeddable-windows env: LEMONADE_VERSION: ${{ needs.build-lemonade-embeddable-windows.outputs.version }} steps: - name: Download embeddable archive uses: actions/download-artifact@v4 with: name: lemonade-embeddable-windows path: . - name: Validate archive structure shell: PowerShell run: | $ErrorActionPreference = "Stop" $archiveDir = "lemonade-embeddable-$($env:LEMONADE_VERSION)-windows-x64" Expand-Archive -Path "$archiveDir.zip" -DestinationPath . # Verify expected files $expected = @( "$archiveDir\lemond.exe", "$archiveDir\lemonade.exe", "$archiveDir\LICENSE", "$archiveDir\resources\server_models.json", "$archiveDir\resources\backend_versions.json", "$archiveDir\resources\defaults.json" ) $failures = @() foreach ($f in $expected) { if (-not (Test-Path $f)) { Write-Host "ERROR: $f not found!" -ForegroundColor Red $failures += $f } } if ($failures.Count -gt 0) { exit 1 } Write-Host "All expected files present" -ForegroundColor Green # Verify NO forbidden files $forbidden = @( "$archiveDir\lemonade-server.exe", "$archiveDir\LemonadeServer.exe", "$archiveDir\lemonade-app.exe", "$archiveDir\lemonade-tray.exe", "$archiveDir\resources\web-app" ) foreach ($f in $forbidden) { if (Test-Path $f) { Write-Host "ERROR: Forbidden file found: $f" -ForegroundColor Red exit 1 } } Write-Host "No forbidden files found" -ForegroundColor Green - name: Test version commands shell: PowerShell run: | $ErrorActionPreference = "Stop" $archiveDir = "lemonade-embeddable-$($env:LEMONADE_VERSION)-windows-x64" & "$archiveDir\lemond.exe" --version & "$archiveDir\lemonade.exe" --version - name: Test lemond startup and health check shell: PowerShell run: | $ErrorActionPreference = "Stop" $archiveDir = "lemonade-embeddable-$($env:LEMONADE_VERSION)-windows-x64" Push-Location $archiveDir $proc = Start-Process -FilePath ".\lemond.exe" -ArgumentList ".\" -PassThru -NoNewWindow # Wait for server to become healthy $healthy = $false for ($i = 0; $i -lt 15; $i++) { try { $response = Invoke-WebRequest -Uri "http://localhost:13305/api/v1/health" -UseBasicParsing -TimeoutSec 2 if ($response.StatusCode -eq 200) { Write-Host "Server is healthy!" -ForegroundColor Green Write-Host "Health response: $($response.Content)" $healthy = $true break } } catch {} Start-Sleep -Seconds 2 } if (-not $healthy) { Write-Host "ERROR: Server failed to start within 30 seconds" -ForegroundColor Red Stop-Process $proc -Force -ErrorAction SilentlyContinue Pop-Location exit 1 } Stop-Process $proc -Force -ErrorAction SilentlyContinue Pop-Location Write-Host "Embeddable Windows smoke test PASSED!" -ForegroundColor Green test-dmg-inference: name: Test .dmg - llamacpp (metal) runs-on: macos-latest needs: build-lemonade-macos-dmg # Skip inference tests when signing is enabled (tag pushes or manual workflow_dispatch) # Also skip when no signing secrets (tests already ran inline in build job) if: ${{ needs.build-lemonade-macos-dmg.outputs.has_signing == 'true' && !startsWith(github.ref, 'refs/tags/') && inputs.enable_signing != true }} env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} LEMONADE_VERSION: ${{ needs.build-lemonade-macos-dmg.outputs.version }} GGML_METAL_NO_RESIDENCY: "1" steps: - uses: actions/checkout@v5 - name: Set HF_HOME environment variable run: echo "HF_HOME=$PWD/hf-cache" >> $GITHUB_ENV - name: Install Lemonade Server (.pkg) uses: ./.github/actions/install-lemonade-server-dmg with: version: ${{ env.LEMONADE_VERSION }} - name: Setup Python and virtual environment uses: ./.github/actions/setup-venv with: venv-name: '.venv' python-version: '3.10' requirements-file: 'test/requirements.txt' - name: Test llamacpp (metal) env: HF_HOME: ${{ env.HF_HOME }} run: | set -e .venv/bin/python test/server_llm.py --wrapped-server llamacpp --backend metal --server-binary /usr/local/bin/lemonade-server - name: Capture and upload server logs if: always() uses: ./.github/actions/capture-server-logs with: artifact-name: server-logs-dmg-llamacpp # ======================================================================== # CLI AND ENDPOINTS TESTS - Run on GitHub-hosted runners (no GPU needed) # ======================================================================== test-cli-endpoints-linux: name: Test ${{ matrix.test_type }} (ubuntu-latest) runs-on: ubuntu-latest needs: build-lemonade-deb container: image: ghcr.io/lemonade-sdk/lemonade/build-environment:ubuntu24.04 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} strategy: fail-fast: false matrix: test_type: [cli, endpoints, ollama, llamacpp-system, streaming-errors, env-vars] env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} LEMONADE_VERSION: ${{ needs.build-lemonade-deb.outputs.version }} steps: - uses: actions/checkout@v5 - name: Configure git safe directory run: git config --global --add safe.directory $(pwd) - name: Install pip and test dependencies shell: bash run: | set -e # Install pip if not already available if ! command -v pip3 &> /dev/null; then apt-get update apt-get install -y python3-pip python3-venv fi # Create venv and install test dependencies python3 -m venv .venv .venv/bin/pip install --upgrade pip .venv/bin/pip install -r test/requirements.txt - name: Install Lemonade (.deb) uses: ./.github/actions/install-lemonade-deb with: version: ${{ env.LEMONADE_VERSION }} - name: Set environment shell: bash run: | echo "HF_HOME=$PWD/hf-cache" >> $GITHUB_ENV echo "VENV_PYTHON=.venv/bin/python" >> $GITHUB_ENV echo "SERVER_BINARY=lemonade-server" >> $GITHUB_ENV - name: Run tests shell: bash run: | set -e VENV_PYTHON=.venv/bin/python SERVER_BINARY=lemonade-server if [ "${{ matrix.test_type }}" = "cli" ]; then echo "Running CLI tests..." $VENV_PYTHON test/server_cli.py --server-binary "$SERVER_BINARY" $VENV_PYTHON test/server_cli.py --server-binary "$SERVER_BINARY" --ephemeral $VENV_PYTHON test/server_cli.py --server-binary "$SERVER_BINARY" --listen-all $VENV_PYTHON test/server_cli.py --server-binary "$SERVER_BINARY" --api-key $VENV_PYTHON test/server_cli2.py --server-binary "$SERVER_BINARY" elif [ "${{ matrix.test_type }}" = "endpoints" ]; then echo "Running endpoint tests..." $VENV_PYTHON test/server_endpoints.py --server-binary "$SERVER_BINARY" $VENV_PYTHON test/server_endpoints.py --server-binary "$SERVER_BINARY" --server-per-test elif [ "${{ matrix.test_type }}" = "ollama" ]; then echo "Running Ollama API tests..." $VENV_PYTHON test/test_ollama.py --server-binary "$SERVER_BINARY" elif [ "${{ matrix.test_type }}" = "llamacpp-system" ]; then echo "Running LlamaCpp System Backend tests..." $VENV_PYTHON test/test_llamacpp_system_backend.py --server-binary "$SERVER_BINARY" elif [ "${{ matrix.test_type }}" = "streaming-errors" ]; then echo "Running streaming error termination tests..." $VENV_PYTHON test/server_streaming_errors.py --server-binary "$SERVER_BINARY" elif [ "${{ matrix.test_type }}" = "env-vars" ]; then echo "Running environment variable tests..." $VENV_PYTHON test/server_env_vars.py --lemond-binary ./deb-extract/usr/bin/lemond fi echo "${{ matrix.test_type }} tests PASSED!" test-cli-endpoints: name: Test ${{ matrix.test_type }} (${{ matrix.os }}) runs-on: ${{ matrix.os }} needs: - build-lemonade-server-installer - build-lemonade-macos-dmg strategy: fail-fast: false matrix: os: [windows-latest, macos-latest] test_type: [cli, endpoints, ollama, llamacpp-system, streaming-errors] include: - os: macos-latest test_type: env-vars env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 GGML_METAL_NO_RESIDENCY: "1" GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} LEMONADE_VERSION: ${{ needs.build-lemonade-macos-dmg.outputs.version }} steps: - uses: actions/checkout@v5 # ---- Windows Setup ---- - name: Setup (Windows) if: runner.os == 'Windows' shell: powershell run: | $cwd = (Get-Item .).FullName echo "HF_HOME=$cwd\hf-cache" >> $Env:GITHUB_ENV echo "LEMONADE_INSTALL_PATH=$cwd\lemonade_server_install" >> $Env:GITHUB_ENV - name: Install Lemonade Server (Windows) if: runner.os == 'Windows' uses: ./.github/actions/install-lemonade-server-msi with: install-path: ${{ env.LEMONADE_INSTALL_PATH }} - name: Set paths (Windows) if: runner.os == 'Windows' shell: powershell run: | echo "VENV_PYTHON=.venv/Scripts/python.exe" >> $Env:GITHUB_ENV echo "SERVER_BINARY=$Env:LEMONADE_INSTALL_PATH\bin\lemonade-server.exe" >> $Env:GITHUB_ENV # ---- macOS Setup (only when signed .pkg is available) ---- - name: Download .pkg package if: runner.os == 'macOS' && needs.build-lemonade-macos-dmg.outputs.has_signing == 'true' uses: actions/download-artifact@v7 with: name: lemonade-macos-pkg path: . - name: Install Lemonade Server (.pkg) id: install-pkg if: runner.os == 'macOS' && needs.build-lemonade-macos-dmg.outputs.has_signing == 'true' uses: ./.github/actions/install-lemonade-server-dmg with: version: ${{ env.LEMONADE_VERSION }} download-artifact: 'false' - name: Set environment (macOS) if: runner.os == 'macOS' && needs.build-lemonade-macos-dmg.outputs.has_signing == 'true' shell: bash run: | echo "HF_HOME=$PWD/hf-cache" >> $GITHUB_ENV echo "VENV_PYTHON=.venv/bin/python" >> $GITHUB_ENV echo "SERVER_BINARY=${{ steps.install-pkg.outputs.bin-path }}/lemonade-server" >> $GITHUB_ENV # ---- Common Setup ---- - name: Setup Python and virtual environment uses: ./.github/actions/setup-venv with: venv-name: '.venv' python-version: '3.10' requirements-file: 'test/requirements.txt' # ---- Run Tests ---- - name: Run ${{ matrix.test_type }} tests if: ${{ !(runner.os == 'macOS' && needs.build-lemonade-macos-dmg.outputs.has_signing != 'true') }} shell: bash env: HF_HOME: ${{ env.HF_HOME }} run: | set -e # Exit on error if [ "${{ matrix.test_type }}" = "cli" ]; then echo "Running CLI tests..." $VENV_PYTHON test/server_cli.py --server-binary "$SERVER_BINARY" $VENV_PYTHON test/server_cli2.py --server-binary "$SERVER_BINARY" elif [ "${{ matrix.test_type }}" = "endpoints" ]; then echo "Running endpoint tests..." $VENV_PYTHON test/server_endpoints.py --server-binary "$SERVER_BINARY" elif [ "${{ matrix.test_type }}" = "ollama" ]; then echo "Running Ollama API tests..." $VENV_PYTHON test/test_ollama.py --server-binary "$SERVER_BINARY" elif [ "${{ matrix.test_type }}" = "llamacpp-system" ]; then echo "Running LlamaCpp System Backend tests..." $VENV_PYTHON test/test_llamacpp_system_backend.py --server-binary "$SERVER_BINARY" elif [ "${{ matrix.test_type }}" = "streaming-errors" ]; then echo "Running streaming error termination tests..." $VENV_PYTHON test/server_streaming_errors.py --server-binary "$SERVER_BINARY" elif [ "${{ matrix.test_type }}" = "env-vars" ]; then echo "Running environment variable tests..." LEMOND_BINARY="$(dirname "$SERVER_BINARY")/lemond" $VENV_PYTHON test/server_env_vars.py --lemond-binary "$LEMOND_BINARY" fi echo "${{ matrix.test_type }} tests PASSED!" - name: Capture and upload server logs if: always() uses: ./.github/actions/capture-server-logs with: artifact-name: server-logs-${{ matrix.os }}-${{ matrix.test_type }} # ======================================================================== # API KEY TESTS - Separate job with LEMONADE_API_KEY env var # ======================================================================== test-cli-apikey-linux: name: Test API Key (ubuntu-latest) runs-on: ubuntu-latest needs: build-lemonade-deb container: image: ghcr.io/lemonade-sdk/lemonade/build-environment:ubuntu24.04 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} env: LEMONADE_CI_MODE: "True" LEMONADE_API_KEY: "test-api-key-12345" PYTHONIOENCODING: utf-8 GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} LEMONADE_VERSION: ${{ needs.build-lemonade-deb.outputs.version }} steps: - uses: actions/checkout@v5 - name: Configure git safe directory run: git config --global --add safe.directory $(pwd) - name: Install pip and test dependencies shell: bash run: | set -e if ! command -v pip3 &> /dev/null; then apt-get update apt-get install -y python3-pip python3-venv fi python3 -m venv .venv .venv/bin/pip install --upgrade pip .venv/bin/pip install -r test/requirements.txt - name: Install Lemonade (.deb) uses: ./.github/actions/install-lemonade-deb with: version: ${{ env.LEMONADE_VERSION }} - name: Verify API key enforcement shell: bash run: | set -e PASS=0 FAIL=0 # 1. No API key โ†’ must get 401 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:13305/api/v1/health 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "401" ]; then echo "PASS: No API key โ†’ $HTTP_CODE" PASS=$((PASS+1)) else echo "FAIL: No API key โ†’ $HTTP_CODE (expected 401)" FAIL=$((FAIL+1)) fi # 2. Wrong API key โ†’ must get 401 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer wrong-key" http://127.0.0.1:13305/api/v1/health 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "401" ]; then echo "PASS: Wrong API key โ†’ $HTTP_CODE" PASS=$((PASS+1)) else echo "FAIL: Wrong API key โ†’ $HTTP_CODE (expected 401)" FAIL=$((FAIL+1)) fi # 3. Correct API key โ†’ must get 200 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $LEMONADE_API_KEY" http://127.0.0.1:13305/api/v1/health 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "200" ]; then echo "PASS: Correct API key โ†’ $HTTP_CODE" PASS=$((PASS+1)) else echo "FAIL: Correct API key โ†’ $HTTP_CODE (expected 200)" FAIL=$((FAIL+1)) fi echo "" echo "Results: $PASS passed, $FAIL failed" if [ "$FAIL" -gt 0 ]; then echo "ERROR: API key enforcement tests failed" exit 1 fi - name: Run CLI tests with API key shell: bash run: | set -e # CLI commands should work because LEMONADE_API_KEY is in the env # and the CLI client reads it automatically .venv/bin/python test/server_cli.py --server-binary lemonade-server - name: Capture and upload server logs if: always() uses: ./.github/actions/capture-server-logs with: artifact-name: server-logs-apikey-ubuntu-latest test-cli-apikey: name: Test API Key (windows-latest) runs-on: windows-latest needs: - build-lemonade-server-installer env: LEMONADE_CI_MODE: "True" LEMONADE_API_KEY: "test-api-key-12345" PYTHONIOENCODING: utf-8 GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} steps: - uses: actions/checkout@v5 - name: Setup (Windows) shell: powershell run: | $cwd = (Get-Item .).FullName echo "HF_HOME=$cwd\hf-cache" >> $Env:GITHUB_ENV echo "LEMONADE_INSTALL_PATH=$cwd\lemonade_server_install" >> $Env:GITHUB_ENV - name: Install Lemonade Server (Windows) uses: ./.github/actions/install-lemonade-server-msi with: install-path: ${{ env.LEMONADE_INSTALL_PATH }} - name: Set paths (Windows) shell: powershell run: | echo "VENV_PYTHON=.venv/Scripts/python.exe" >> $Env:GITHUB_ENV echo "SERVER_BINARY=$Env:LEMONADE_INSTALL_PATH\bin\lemonade-server.exe" >> $Env:GITHUB_ENV - name: Setup Python and virtual environment uses: ./.github/actions/setup-venv with: venv-name: '.venv' python-version: '3.10' requirements-file: 'test/requirements.txt' - name: Verify API key enforcement shell: bash run: | set -e PASS=0 FAIL=0 # 1. No API key โ†’ must get 401 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:13305/api/v1/health 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "401" ]; then echo "PASS: No API key โ†’ $HTTP_CODE" PASS=$((PASS+1)) else echo "FAIL: No API key โ†’ $HTTP_CODE (expected 401)" FAIL=$((FAIL+1)) fi # 2. Wrong API key โ†’ must get 401 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer wrong-key" http://localhost:13305/api/v1/health 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "401" ]; then echo "PASS: Wrong API key โ†’ $HTTP_CODE" PASS=$((PASS+1)) else echo "FAIL: Wrong API key โ†’ $HTTP_CODE (expected 401)" FAIL=$((FAIL+1)) fi # 3. Correct API key โ†’ must get 200 HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $LEMONADE_API_KEY" http://localhost:13305/api/v1/health 2>/dev/null || echo "000") if [ "$HTTP_CODE" = "200" ]; then echo "PASS: Correct API key โ†’ $HTTP_CODE" PASS=$((PASS+1)) else echo "FAIL: Correct API key โ†’ $HTTP_CODE (expected 200)" FAIL=$((FAIL+1)) fi echo "" echo "Results: $PASS passed, $FAIL failed" if [ "$FAIL" -gt 0 ]; then echo "ERROR: API key enforcement tests failed" exit 1 fi - name: Run CLI tests with API key shell: bash env: HF_HOME: ${{ env.HF_HOME }} run: | set -e $VENV_PYTHON test/server_cli.py --server-binary "$SERVER_BINARY" - name: Capture and upload server logs if: always() uses: ./.github/actions/capture-server-logs with: artifact-name: server-logs-apikey-windows-latest # ======================================================================== # RELEASE JOB - Add artifacts to GitHub release # ======================================================================== release: name: Create GitHub Release runs-on: ubuntu-latest needs: - sign-msi-installers - build-lemonade-rpm - build-lemonade-macos-dmg - build-lemonade-appimage - build-lemonade-embeddable-linux - build-lemonade-embeddable-windows - test-cli-endpoints - test-rpm-package - test-embeddable-linux - test-embeddable-windows if: startsWith(github.ref, 'refs/tags/v') env: LEMONADE_VERSION: ${{ needs.build-lemonade-rpm.outputs.version }} steps: - name: Checkout for release notes action uses: actions/checkout@v5 with: sparse-checkout: .github - name: Download Signed Lemonade Server Installer (Windows) uses: actions/download-artifact@v7 with: name: Lemonade_Server_MSI_Signed path: . - name: Download Lemonade .rpm Package uses: actions/download-artifact@v7 with: name: lemonade-rpm path: . - name: Download Lemonade macOS .pkg Package uses: actions/download-artifact@v7 with: name: lemonade-macos-pkg path: . - name: Download Lemonade AppImage (Linux) uses: actions/download-artifact@v7 with: name: lemonade-appimage path: . - name: Download Lemonade Embeddable (Linux) uses: actions/download-artifact@v4 with: name: lemonade-embeddable-linux path: . - name: Download Lemonade Embeddable (Windows) uses: actions/download-artifact@v4 with: name: lemonade-embeddable-windows path: . - name: Verify release artifacts run: | echo "Release artifacts:" ls -lh lemonade-server-minimal.msi ls -lh lemonade.msi ls -lh lemonade-server-${LEMONADE_VERSION}.x86_64.rpm ls -lh *.pkg ls -lh lemonade-app-${LEMONADE_VERSION}-x86_64.AppImage ls -lh lemonade-embeddable-${LEMONADE_VERSION}-ubuntu-x64.tar.gz ls -lh lemonade-embeddable-${LEMONADE_VERSION}-windows-x64.zip - name: Generate release notes id: release-notes uses: ./.github/actions/generate-release-notes with: version: ${{ env.LEMONADE_VERSION }} repo: ${{ github.repository }} github_token: ${{ secrets.GITHUB_TOKEN }} - name: Create Release uses: softprops/action-gh-release@v2 with: name: ${{ github.ref_name }} body_path: ${{ steps.release-notes.outputs.release_notes_file }} files: | lemonade-server-minimal.msi lemonade.msi lemonade-server-${{ env.LEMONADE_VERSION }}.x86_64.rpm *.pkg lemonade-app-${{ env.LEMONADE_VERSION }}-x86_64.AppImage lemonade-embeddable-${{ env.LEMONADE_VERSION }}-ubuntu-x64.tar.gz lemonade-embeddable-${{ env.LEMONADE_VERSION }}-windows-x64.zip fail_on_unmatched_files: true lemonade-sdk-lemonade-dbde812/.github/workflows/docker-build-smoke-test.yml000066400000000000000000000040031516551144000271060ustar00rootroot00000000000000name: Docker Build Smoke Test on: pull_request: paths: - "Dockerfile" - ".dockerignore" - "src/cpp/**" - "include/**" - "resources/**" - "CMakeLists.txt" - "CMakePresets.json" - "setup.sh" - ".github/workflows/docker-build-smoke-test.yml" permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: docker-smoke-test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v5 - name: Build Docker image run: | docker build -t lemonade:test . - name: Run container run: | docker run -d \ --name lemonade-test \ -p 13305:13305 \ lemonade:test - name: Wait for server to be ready run: | echo "Waiting for Lemonade to start..." for i in {1..30}; do if curl -sf http://localhost:13305/live > /dev/null; then echo "Server is up" exit 0 fi sleep 2 done echo "Server did not start in time" docker logs lemonade-test exit 1 - name: Load model run: | curl -X POST http://localhost:13305/api/v1/load \ -H "Content-Type: application/json" \ -d '{"model_name": "Qwen3-0.6B-GGUF"}' - name: Chat completion smoke test run: | RESPONSE=$(curl -s http://localhost:13305/api/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "Qwen3-0.6B-GGUF", "messages": [ { "role": "user", "content": "Hello, Lemonade!" } ] }') echo "Response:" echo "$RESPONSE" echo "$RESPONSE" | grep -q '"choices"' || exit 1 - name: Cleanup if: always() run: | docker logs lemonade-test || true docker rm -f lemonade-test || true lemonade-sdk-lemonade-dbde812/.github/workflows/docs_and_style.yml000066400000000000000000000017611516551144000254530ustar00rootroot00000000000000name: Docs And Style on: push: branches: ["main"] pull_request: merge_group: permissions: contents: read jobs: markdown-link-check: runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true steps: - uses: actions/checkout@v5 - name: Setup Node.js uses: actions/setup-node@v5 with: node-version: '20' - name: Install markdown-link-check run: npm install -g markdown-link-check - name: Check Markdown links shell: bash run: | # To run this manually on Windows, run: # npm install -g markdown-link-check # Get-ChildItem -Recurse -Filter *.md | ForEach-Object { markdown-link-check $_.FullName -c .\.github\workflows\mlc_config.json } set -euo pipefail find . -type f -name "*.md" -not -path "./node_modules/*" \ -exec markdown-link-check -c .github/workflows/mlc_config.json {} \; lemonade-sdk-lemonade-dbde812/.github/workflows/launchpad-ppa.yml000066400000000000000000000103301516551144000251660ustar00rootroot00000000000000name: Launchpad PPA permissions: contents: read on: push: branches: - main tags: - '*' pull_request: branches: - main workflow_dispatch: jobs: prepare-matrix: name: Prepare matrix runs-on: ubuntu-latest outputs: package_matrix: ${{ steps.set-matrix.outputs.package_matrix }} steps: - name: Define shared matrix values id: set-matrix run: | { echo 'package_matrix<> "$GITHUB_OUTPUT" package: name: Build needs: prepare-matrix runs-on: ubuntu-latest permissions: contents: read strategy: matrix: ${{ fromJSON(needs.prepare-matrix.outputs.package_matrix) }} container: image: ghcr.io/lemonade-sdk/lemonade/build-environment:${{ matrix.distro }}${{ matrix.release }} credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout code uses: actions/checkout@v5 with: submodules: recursive fetch-depth: 0 fetch-tags: true - name: Configure git safe directory run: git config --global --add safe.directory $(pwd) - name: Prepare Debian build id: get_version uses: ./.github/actions/prepare-debian-build with: release: ${{ matrix.release }} codename: ${{ matrix.codename }} - name: Get short SHA id: short_sha run: echo "sha=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT - name: Catch missing build-deps run: | apt-get update && apt-get build-dep . -y - name: Build binary package id: build_binary if: github.event_name == 'pull_request' uses: ./.github/actions/build-debian-package with: package-type: binary deb-version: ${{ steps.get_version.outputs.version }} - name: Build source package id: build_source if: github.event_name != 'pull_request' uses: ./.github/actions/build-debian-package with: package-type: source deb-version: ${{ steps.get_version.outputs.version }} - name: Upload Debian package if: github.event_name == 'pull_request' uses: actions/upload-artifact@v7 with: name: lemonade-${{ matrix.codename }}-deb path: ${{ steps.build_binary.outputs.output-dir }}/*.deb retention-days: 30 - name: Upload source package if: github.event_name != 'pull_request' uses: actions/upload-artifact@v7 with: name: lemonade-${{ matrix.codename }}-source-package path: ${{ steps.build_source.outputs.output-dir }}/* retention-days: 30 upload-stable: name: Upload (Stable) strategy: matrix: ${{ fromJSON(needs.prepare-matrix.outputs.package_matrix) }} needs: [prepare-matrix, package] permissions: contents: read if: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name != 'pull_request' }} uses: ./.github/workflows/upload.yml with: target: ppa:lemonade-team/stable artifact: lemonade-${{ matrix.codename }}-source-package secrets: inherit upload-bleeding-edge: name: Upload (Bleeding Edge) strategy: matrix: ${{ fromJSON(needs.prepare-matrix.outputs.package_matrix) }} needs: [prepare-matrix, package] permissions: contents: read if: ${{ !startsWith(github.ref, 'refs/tags/v') && github.event_name != 'pull_request' }} uses: ./.github/workflows/upload.yml with: target: ppa:lemonade-team/bleeding-edge artifact: lemonade-${{ matrix.codename }}-source-package secrets: inherit lemonade-sdk-lemonade-dbde812/.github/workflows/linux_distro_builds.yml000066400000000000000000000115311516551144000265420ustar00rootroot00000000000000name: Linux Distro Builds ๐Ÿง on: push: branches: ["main"] pull_request: merge_group: workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-linux: name: Build on ${{ matrix.distro }} runs-on: ubuntu-latest container: image: ${{ matrix.image }} strategy: fail-fast: false matrix: include: - distro: Arch image: archlinux:latest python: python - distro: Debian image: debian:trixie python: python3 - distro: Fedora image: fedora:latest python: python3 - distro: openSUSE image: opensuse/tumbleweed:latest python: python3 steps: - name: Checkout code uses: actions/checkout@v5 with: clean: true fetch-depth: 0 - name: Run setup.sh to configure environment run: | set -e echo "Running setup.sh for ${{ matrix.distro }}..." bash setup.sh echo "Verifying process management tools..." command -v pgrep && command -v pkill && command -v ps echo "Verifying SSL certificates..." ls -la /etc/ssl/certs/ | head -10 echo "Testing HTTPS connectivity..." curl -sI https://huggingface.co | head -5 || echo "Warning: HTTPS test failed" echo "Setup completed successfully!" - name: Build C++ Server with CMake run: | set -e echo "Building lemond and lemonade on ${{ matrix.distro }}..." # Build echo "Building binaries..." cmake --build --preset default # Verify binaries exist if [ ! -f "build/lemond" ]; then echo "ERROR: lemond not found!" echo "Build directory contents:" ls -lh build/ exit 1 fi if [ ! -f "build/lemonade" ]; then echo "ERROR: lemonade not found!" echo "Build directory contents:" ls -lh build/ exit 1 fi echo "Binaries found successfully" ls -lh build/lemond build/lemonade echo "Verifying binary executability..." ./build/lemond --version ./build/lemonade --version echo "${{ matrix.distro }} build successful!" - name: Verify system library linking run: | echo "Checking library dependencies..." cd build echo "=== lemond dependencies ===" ldd lemond echo "" echo "=== lemonade dependencies ===" ldd lemonade echo "" echo "All dependencies resolved successfully on ${{ matrix.distro }}!" - name: Setup Python virtual environment run: | echo "Creating Python virtual environment..." if [ "${{ matrix.distro }}" = "Debian" ]; then apt install -y python3-venv fi ${{ matrix.python }} -m venv .venv echo "Activating virtual environment and installing dependencies..." . .venv/bin/activate pip install --upgrade pip pip install -r test/requirements.txt echo "Python environment setup complete!" - name: Start server env: LEMONADE_CI_MODE: "True" run: | echo "Starting lemond in background..." ./build/lemond --port 13305 > /tmp/lemonade-server.log 2>&1 & echo "Waiting for server to be ready..." for i in $(seq 1 30); do if curl -sf http://localhost:13305/live > /dev/null 2>&1; then echo "Server is running and healthy" exit 0 fi sleep 2 done echo "ERROR: Server did not start" cat /tmp/lemonade-server.log 2>/dev/null || true exit 1 - name: Run CLI tests env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 run: | set -e . .venv/bin/activate SERVER_BINARY="$(pwd)/build/lemonade" echo "Running CLI tests..." python test/server_cli.py --server-binary "$SERVER_BINARY" echo "CLI tests PASSED!" - name: Run endpoint tests env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 run: | set -e . .venv/bin/activate SERVER_BINARY="$(pwd)/build/lemonade" echo "Running endpoint tests..." python test/server_endpoints.py --server-binary "$SERVER_BINARY" echo "Endpoint tests PASSED!" - name: Capture and upload server logs if: always() uses: ./.github/actions/capture-server-logs with: artifact-name: server-logs-distro-${{ matrix.distro }} lemonade-sdk-lemonade-dbde812/.github/workflows/mlc_config.json000066400000000000000000000003151516551144000247230ustar00rootroot00000000000000{ "ignorePatterns": [ { "pattern": "@amd.com" }, { "pattern": "localhost" }, {"pattern":"^https?://"} ], "timeout": "10s", "retryOn429": true, "retryCount": 3 } lemonade-sdk-lemonade-dbde812/.github/workflows/monitor_selfhosted_runners.yml000066400000000000000000000135261516551144000301460ustar00rootroot00000000000000name: Monitor Self-Hosted Runners on: schedule: - cron: "0 1 * * 1" # every Monday at 1AM UTC workflow_dispatch: permissions: actions: read # Required to read artifacts jobs: check: runs-on: ubuntu-latest steps: - name: Get list of runners id: runners run: | # Try to get repo-level runners first curl -s \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ https://api.github.com/repos/${{ github.repository }}/actions/runners \ > repo_runners.json # Also get org-level runners curl -s \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ https://api.github.com/orgs/${{ github.repository_owner }}/actions/runners \ > org_runners.json # Combine and deduplicate runner names repo_names=$(jq -r '.runners[]?.name // empty' repo_runners.json 2>/dev/null || echo "") org_names=$(jq -r '.runners[]?.name // empty' org_runners.json 2>/dev/null || echo "") # Fallback to known runner list if API fails known_runners="jfowers-stx-01,jfowers-stx-02,sjlab-stx-halo-06,sjlab-stx-halo-07,sjlab-stx-halo-08,sjlab-stx-halo-09,sjlab-stx-halo-10,sjlab-stx-halo-11,sjlab-stx-halo-12,sjlab-stx-halo-13,sjlab-stx-0" if [ -n "$repo_names" ] || [ -n "$org_names" ]; then all_names=$(echo -e "$repo_names\n$org_names" | sort -u | tr '\n' ',' | sed 's/,$//') echo "names=$all_names" >> $GITHUB_OUTPUT echo "Using API discovered runners: $all_names" else echo "names=$known_runners" >> $GITHUB_OUTPUT echo "Using fallback runner list: $known_runners" fi - name: Get artifacts id: artifacts run: | # Fetch all artifacts with pagination echo '{"artifacts":[]}' > artifacts.json page=1 while true; do response=$(curl -s \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ "https://api.github.com/repos/${{ github.repository }}/actions/artifacts?per_page=100&page=$page") # Check if request was successful and has artifacts artifact_count=$(echo "$response" | jq '.artifacts | length') if [ "$artifact_count" -eq 0 ] || [ "$artifact_count" = "null" ]; then break fi # Merge artifacts into our main file jq -s '.[0].artifacts + .[1].artifacts | {"artifacts": .}' artifacts.json <(echo "$response") > temp.json mv temp.json artifacts.json page=$((page + 1)) # Safety break after 10 pages (1000 artifacts) if [ $page -gt 10 ]; then break fi done - name: Check each runner heartbeat id: check run: | missing="" now=$(date -u +%s) echo "Current time: $(date -u)" echo "Total artifacts found: $(jq '.artifacts | length' artifacts.json)" echo "Heartbeat artifacts found: $(jq '[.artifacts[] | select(.name | startswith("heartbeat-"))] | length' artifacts.json)" for r in $(echo "${{ steps.runners.outputs.names }}" | tr ',' ' '); do [ -z "$r" ] && continue echo "Checking $r" ts=$(jq -r --arg r "$r" '[.artifacts[] | select(.name=="heartbeat-"+$r)] | sort_by(.updated_at) | last | .updated_at // empty' artifacts.json) if [ -z "$ts" ] || [ "$ts" == "null" ]; then echo "No heartbeat found for $r" missing="$missing $r" continue fi echo "Latest heartbeat for $r: $ts" hb=$(date -d "$ts" +%s) diff=$((now - hb)) echo "Age: $diff seconds ($(($diff / 3600)) hours)" if [ $diff -gt 691200 ]; then # 8 days (weekly + 1 day buffer) echo "Heartbeat stale for $r (last seen: $ts)" missing="$missing $r" else echo "Heartbeat OK for $r" fi done echo "missing=$missing" >> $GITHUB_OUTPUT - name: Fail if any runner missing if: steps.check.outputs.missing != '' run: | echo "Missing or stale runners: ${{ steps.check.outputs.missing }}" exit 1 - name: Send Teams alert via Power Automate if: failure() run: | echo "Sending Teams alert via Power Automate..." response=$(curl -s -w "%{http_code}" -X POST \ -H "Content-Type: application/json" \ -d '{ "type": "AdaptiveCard", "version": "1.3", "body": [ { "type": "TextBlock", "text": "โš ๏ธ Self-hosted Runner Alert", "weight": "bolder", "size": "medium", "color": "attention" }, { "type": "TextBlock", "text": "The following runners are offline or missing heartbeats:", "wrap": true }, { "type": "TextBlock", "text": "${{ steps.check.outputs.missing }}", "wrap": true, "fontType": "monospace" } ] }' \ "${{ secrets.TEAMS_WEBHOOK_URL }}") http_code="${response: -3}" response_body="${response%???}" echo "HTTP Status: $http_code" echo "Response: $response_body" if [ "$http_code" != "200" ] && [ "$http_code" != "202" ]; then echo "Power Automate webhook failed with status $http_code" else echo "Teams alert sent successfully via Power Automate" fi lemonade-sdk-lemonade-dbde812/.github/workflows/publish-website.yml000066400000000000000000000035011516551144000255610ustar00rootroot00000000000000name: Publish Website on: push: tags: - v* workflow_dispatch: repository_dispatch: types: [marketplace-updated] jobs: build-n-publish-website: name: Build and publish lemonade-server.ai website runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.10" - name: Create virtual environment and install dependencies run: | python -m venv .venv venvPython=".venv/bin/python" venvPip=".venv/bin/pip" $venvPython -m pip install --upgrade pip $venvPip install -r docs/assets/mkdocs_requirements.txt - name: Configure git run: | git config --global user.name "Lemonade Bot" git config --global user.email "lemonade@amd.com" - name: Capture main branch commit hash run: | echo "MAIN_COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV - name: Merge into website branch run: | git checkout website git merge origin/main -X theirs - name: Update README with marketplace apps run: | venvPython=".venv/bin/python" $venvPython docs/update_readme_marketplace.py - name: Run mkdocs update script run: | venvPython=".venv/bin/python" $venvPython docs/publish_website_docs.py - name: Push updates run: | git add . if git diff --staged --quiet; then echo "No changes to commit, skipping commit step" else git commit -m "Update website for: ${MAIN_COMMIT_HASH}" fi git push lemonade-sdk-lemonade-dbde812/.github/workflows/runner_heartbeat.yml000066400000000000000000000026541516551144000260130ustar00rootroot00000000000000name: Runner Heartbeat on: schedule: - cron: "0 1 * * 0" # every Sunday at 1AM UTC workflow_dispatch: permissions: actions: write # Required to upload artifacts jobs: heartbeat: strategy: matrix: runner: - jfowers-stx-01 - jfowers-stx-02 - sjlab-stx-halo-06 - sjlab-stx-halo-07 - sjlab-stx-halo-08 - sjlab-stx-halo-09 - sjlab-stx-halo-10 - sjlab-stx-halo-11 - sjlab-stx-halo-12 - sjlab-stx-halo-13 - sjlab-stx-0 fail-fast: false # Continue even if one runner fails runs-on: [self-hosted, "${{ matrix.runner }}"] steps: - name: Create heartbeat file (Windows) if: runner.os == 'Windows' shell: powershell run: | $timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ssK" $runner = "${{ runner.name }}" "$timestamp" | Out-File -FilePath "heartbeat-$runner.txt" -Encoding utf8 - name: Create heartbeat file (Linux) if: runner.os == 'Linux' shell: bash run: | timestamp=$(date -Iseconds) runner="${{ runner.name }}" echo "$timestamp" > "heartbeat-$runner.txt" - name: Upload heartbeat artifact uses: actions/upload-artifact@v7 with: name: heartbeat-${{ runner.name }} path: heartbeat-${{ runner.name }}.txt retention-days: 14 lemonade-sdk-lemonade-dbde812/.github/workflows/test-new-runner.yml000066400000000000000000000022261516551144000255330ustar00rootroot00000000000000name: Test New Runner on Windows permissions: contents: read on: workflow_dispatch: inputs: runner: description: 'Runner to use' required: true type: choice options: - test default: 'test' jobs: test-runner: runs-on: ${{ inputs.runner }} steps: - name: Checkout code uses: actions/checkout@v5 - name: Show runner information run: | echo "Hello from the runner!" echo "Runner name: ${{ runner.name }}" echo "Runner OS: ${{ runner.os }}" echo "Runner arch: ${{ runner.arch }}" hostname - name: Install Lemonade run: | Invoke-WebRequest -Uri "https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade.msi" -OutFile "lemonade.msi" -UseBasicParsing msiexec /i lemonade.msi /qn /L*V install.log Start-Sleep -Seconds 5 - name: Refresh Environment and Test run: | $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") lemonade-server --help lemonade-sdk-lemonade-dbde812/.github/workflows/test_release_notes.yml000066400000000000000000000026141516551144000263460ustar00rootroot00000000000000name: Test Release Notes Formatting ๐Ÿ“ on: workflow_dispatch: permissions: contents: write jobs: test-release-notes: name: Preview Release Notes runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: fetch-depth: 0 # Use the latest existing tag to test with real merged PR data - name: Get latest tag id: latest-tag run: | LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") if [ -n "$LATEST_TAG" ]; then # Extract version number (remove 'v' prefix) VERSION="${LATEST_TAG#v}" echo "Using existing tag $LATEST_TAG for testing" else # Fallback to CMakeLists version VERSION=$(grep -Po 'project\(lemon_cpp VERSION \K[0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt) echo "No tags found, using CMakeLists version $VERSION" fi echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Generate release notes uses: ./.github/actions/generate-release-notes with: version: ${{ steps.latest-tag.outputs.version }} repo: ${{ github.repository }} github_token: ${{ secrets.GITHUB_TOKEN }} - name: Upload preview uses: actions/upload-artifact@v7 with: name: release-notes-preview path: release_notes.md retention-days: 7 lemonade-sdk-lemonade-dbde812/.github/workflows/upload.yml000066400000000000000000000023231516551144000237400ustar00rootroot00000000000000name: PPA Upload on: workflow_call: inputs: target: required: true type: string artifact: required: true type: string permissions: contents: read jobs: upload-ppa: runs-on: ubuntu-24.04 name: Upload to PPA steps: - name: Download source package uses: actions/download-artifact@v7 with: name: ${{ inputs.artifact }} - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y devscripts dput --no-install-recommends - name: Setup GPG key run: | echo "Setting up GPG key..." echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --import --batch --yes # Get the key ID GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | head -1 | awk '{print $2}' | cut -d'/' -f2) echo "GPG_KEY_ID=$GPG_KEY_ID" >> $GITHUB_ENV - name: Sign source package run: | echo "Signing package with GPG key $GPG_KEY_ID..." debsign -k $GPG_KEY_ID *.changes - name: Upload to PPA run: | echo "Uploading to ${{ inputs.target }}..." dput ${{ inputs.target }} *.changes lemonade-sdk-lemonade-dbde812/.github/workflows/validate_llamacpp.yml000066400000000000000000000332671516551144000261310ustar00rootroot00000000000000name: Validate New llama.cpp Release on: workflow_dispatch: inputs: lite: description: 'Run in lite mode (smallest model only)' type: boolean default: false pull_request: schedule: # Sunday at 12:00 PM ET (17:00 UTC, 16:00 UTC during EDT) - cron: "0 16 * * 0" permissions: contents: write pull-requests: write env: LEMONADE_CI_MODE: "True" PYTHONIOENCODING: utf-8 LITE_MODE: ${{ github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.lite == true) }} jobs: # ======================================================================== # Discover the latest llama.cpp release for each upstream repo # ======================================================================== get-latest-releases: name: Get latest releases if: github.event_name != 'pull_request' runs-on: ubuntu-latest outputs: llamacpp_release: ${{ steps.llamacpp.outputs.release }} llamacpp_rocm_release: ${{ steps.llamacpp_rocm.outputs.release }} steps: - name: Get latest llama.cpp release id: llamacpp env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | RELEASE=$(gh api repos/ggml-org/llama.cpp/releases/latest --jq '.tag_name') echo "Latest llama.cpp release: $RELEASE" echo "release=$RELEASE" >> "$GITHUB_OUTPUT" - name: Get latest llamacpp-rocm release id: llamacpp_rocm env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | RELEASE=$(gh api repos/lemonade-sdk/llamacpp-rocm/releases/latest --jq '.tag_name') echo "Latest llamacpp-rocm release: $RELEASE" echo "release=$RELEASE" >> "$GITHUB_OUTPUT" # ======================================================================== # Build binaries with updated backend versions # ======================================================================== build: name: Build needs: get-latest-releases if: always() && !failure() && !cancelled() runs-on: windows-latest steps: - uses: actions/checkout@v5 with: clean: true fetch-depth: 0 - name: Update backend_versions.json if: needs.get-latest-releases.result == 'success' shell: PowerShell env: LLAMACPP_RELEASE: ${{ needs.get-latest-releases.outputs.llamacpp_release }} LLAMACPP_ROCM_RELEASE: ${{ needs.get-latest-releases.outputs.llamacpp_rocm_release }} run: | $ErrorActionPreference = "Stop" $path = "src/cpp/resources/backend_versions.json" $data = Get-Content $path -Raw | ConvertFrom-Json $release = $env:LLAMACPP_RELEASE Write-Host "Updating llamacpp vulkan/cpu/metal to $release" -ForegroundColor Cyan foreach ($key in @("vulkan", "cpu", "metal")) { $old = $data.llamacpp.$key $data.llamacpp.$key = $release Write-Host " llamacpp.${key}: $old -> $release" } $rocm = $env:LLAMACPP_ROCM_RELEASE Write-Host "Updating llamacpp rocm to $rocm" -ForegroundColor Cyan $old = $data.llamacpp.rocm $data.llamacpp.rocm = $rocm Write-Host " llamacpp.rocm: $old -> $rocm" $data | ConvertTo-Json -Depth 10 | Set-Content $path -Encoding UTF8 - name: Build C++ Server with CMake shell: PowerShell run: | $ErrorActionPreference = "Stop" Write-Host "Building Lemonade server binaries..." -ForegroundColor Cyan if (Test-Path "build") { Remove-Item -Recurse -Force "build" } cmake --preset windows if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } cmake --build build --config Release if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } if (-not (Test-Path "build\Release\lemond.exe")) { Write-Host "ERROR: lemond.exe not found!" -ForegroundColor Red exit 1 } Write-Host "Build successful!" -ForegroundColor Green - name: Upload build artifacts uses: actions/upload-artifact@v7 with: name: llamacpp-build path: | build/Release/ build/resources/ retention-days: 7 # ======================================================================== # Validate backends on self-hosted runner # ======================================================================== validate: name: Validate ${{ matrix.backend }} needs: [get-latest-releases, build] if: always() && needs.build.result == 'success' runs-on: [self-hosted, Windows, 128gb] strategy: fail-fast: false matrix: backend: [vulkan, rocm] steps: - uses: actions/checkout@v5 with: clean: true - name: Cleanup processes uses: ./.github/actions/cleanup-processes-windows - name: Download build artifacts uses: actions/download-artifact@v7 with: name: llamacpp-build path: build - name: Verify binaries shell: PowerShell run: | $lemondExe = "build\Release\lemond.exe" if (-not (Test-Path $lemondExe)) { Write-Host "ERROR: lemond.exe not found!" -ForegroundColor Red Get-ChildItem -Recurse build | Select-Object FullName exit 1 } & $lemondExe --version Write-Host "Binaries verified!" -ForegroundColor Green - name: Setup Python and virtual environment uses: ./.github/actions/setup-venv with: venv-name: '.venv' python-version: '3.10' requirements-file: 'test/requirements.txt' - name: Run validation with lemond shell: PowerShell run: | $ErrorActionPreference = "Stop" $lemondExe = (Resolve-Path "build\Release\lemond.exe").Path $backend = "${{ matrix.backend }}" $logsDir = "server-logs-$backend" $cacheDir = Join-Path $PWD "ci-cache-$backend" $venvPython = ".\.venv\Scripts\python.exe" New-Item -ItemType Directory -Force -Path $logsDir | Out-Null New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null $stdoutLog = Join-Path $PWD "$logsDir\lemond.stdout.log" $stderrLog = Join-Path $PWD "$logsDir\lemond.stderr.log" # Start lemond (Python script handles readiness polling) $proc = Start-Process ` -FilePath $lemondExe ` -ArgumentList @($cacheDir, "--port", "13305", "--host", "127.0.0.1") ` -RedirectStandardOutput $stdoutLog ` -RedirectStandardError $stderrLog ` -PassThru Write-Host "Started lemond PID $($proc.Id)" -ForegroundColor Cyan try { $validationArgs = @( "test/validate_llamacpp.py", "--backend", $backend, "--output", "llamacpp_validation_$backend.json", "--logs-dir", $logsDir ) if ("${{ env.LITE_MODE }}" -eq "true") { $validationArgs += "--lite" } & $venvPython @validationArgs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } finally { try { Invoke-WebRequest -Uri "http://127.0.0.1:13305/internal/shutdown" ` -Method POST -TimeoutSec 10 | Out-Null Start-Sleep -Seconds 2 } catch { Write-Host "lemond shutdown not reachable; relying on cleanup step." -ForegroundColor Yellow } } - name: Upload results if: always() uses: actions/upload-artifact@v7 with: name: validation-results-${{ matrix.backend }} path: llamacpp_validation_${{ matrix.backend }}.json retention-days: 30 - name: Upload server logs if: always() uses: actions/upload-artifact@v7 with: name: server-logs-${{ matrix.backend }} path: server-logs-${{ matrix.backend }}/ retention-days: 30 if-no-files-found: ignore - name: Cleanup if: always() uses: ./.github/actions/cleanup-processes-windows # ======================================================================== # Create PR if both validations passed (schedule or workflow_dispatch only) # ======================================================================== create-pr: name: Create update PR needs: [get-latest-releases, validate] runs-on: ubuntu-latest if: >- (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && needs.validate.result == 'success' steps: - uses: actions/checkout@v5 - name: Download validation results uses: actions/download-artifact@v7 with: pattern: validation-results-* merge-multiple: true - name: Update backend_versions.json with both releases shell: bash env: LLAMACPP_RELEASE: ${{ needs.get-latest-releases.outputs.llamacpp_release }} LLAMACPP_ROCM_RELEASE: ${{ needs.get-latest-releases.outputs.llamacpp_rocm_release }} run: | python3 -c " import json, os path = 'src/cpp/resources/backend_versions.json' with open(path, 'r') as f: data = json.load(f) llamacpp = os.environ['LLAMACPP_RELEASE'] rocm = os.environ['LLAMACPP_ROCM_RELEASE'] data['llamacpp']['vulkan'] = llamacpp data['llamacpp']['cpu'] = llamacpp data['llamacpp']['metal'] = llamacpp data['llamacpp']['rocm'] = rocm with open(path, 'w') as f: json.dump(data, f, indent=2) f.write('\n') print(f'Updated: vulkan/cpu/metal={llamacpp}, rocm={rocm}') " - name: Generate PR body shell: bash env: LLAMACPP_RELEASE: ${{ needs.get-latest-releases.outputs.llamacpp_release }} LLAMACPP_ROCM_RELEASE: ${{ needs.get-latest-releases.outputs.llamacpp_rocm_release }} run: | python3 << 'PYEOF' import json, os body_lines = [] llamacpp = os.environ.get("LLAMACPP_RELEASE", "unknown") rocm = os.environ.get("LLAMACPP_ROCM_RELEASE", "unknown") body_lines.append("## Auto-update llama.cpp backends") body_lines.append("") body_lines.append(f"- **llama.cpp** (vulkan/cpu/metal): `{llamacpp}`") body_lines.append(f"- **llamacpp-rocm**: `{rocm}`") body_lines.append("") body_lines.append("## Validation Results") body_lines.append("") body_lines.append("| Model | Result | Response | input_tokens | output_tokens | time_to_first_token | tokens_per_second |") body_lines.append("|-------|--------|----------|--------------|---------------|---------------------|-------------------|") for backend in ["vulkan", "rocm"]: results_file = f"llamacpp_validation_{backend}.json" if not os.path.isfile(results_file): body_lines.append(f"| _{backend}: no results_ | | | | | | |") continue with open(results_file, "r") as f: results = json.load(f) for r in results: status = "PASS" if r["pass"] else "FAIL" # Truncate response for table readability resp = str(r.get("response", ""))[:80].replace("|", "\\|").replace("\n", " ") ttft = r.get("time_to_first_token", "N/A") if isinstance(ttft, float): ttft = f"{ttft:.3f}s" tps = r.get("tokens_per_second", "N/A") if isinstance(tps, float): tps = f"{tps:.1f}" body_lines.append( f"| {r['model']} ({backend}) | {status} | {resp} | " f"{r.get('input_tokens', 'N/A')} | {r.get('output_tokens', 'N/A')} | " f"{ttft} | {tps} |" ) body_lines.append("") body_lines.append("---") body_lines.append("*Auto-generated by validate_llamacpp workflow*") body = "\n".join(body_lines) with open("pr_body.md", "w") as f: f.write(body) PYEOF - name: Create Pull Request env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} LLAMACPP_RELEASE: ${{ needs.get-latest-releases.outputs.llamacpp_release }} LLAMACPP_ROCM_RELEASE: ${{ needs.get-latest-releases.outputs.llamacpp_rocm_release }} run: | BRANCH="auto/llamacpp-update-${LLAMACPP_RELEASE}-${LLAMACPP_ROCM_RELEASE}" # Check if a PR already exists for this update EXISTING_PR=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "") if [ -n "$EXISTING_PR" ]; then echo "PR #$EXISTING_PR already exists for branch $BRANCH. Skipping." exit 0 fi git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" # Skip if backend_versions.json is already up to date if git diff --quiet src/cpp/resources/backend_versions.json; then echo "backend_versions.json is unchanged โ€” nothing to update." exit 0 fi git checkout -b "$BRANCH" git add src/cpp/resources/backend_versions.json git commit -m "Update llama.cpp to ${LLAMACPP_RELEASE}, rocm to ${LLAMACPP_ROCM_RELEASE}" git push origin "$BRANCH" gh pr create \ --title "Update llama.cpp to ${LLAMACPP_RELEASE}" \ --body-file pr_body.md \ --base main \ --head "$BRANCH" lemonade-sdk-lemonade-dbde812/.gitignore000066400000000000000000000066471516551144000203410ustar00rootroot00000000000000### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST .DS_Store # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. # Note: Only ignore manifest files in build/dist directories, not source C++ manifests build/*.manifest dist/*.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ .aider* # Claude files CLAUDE.md CLAUDE.local.md .settings.local.json # Bruno/GitHub files .bruno/ # Cursor files (using .cursor/rules/*.mdc format) .cursor/ .cursor** # WiX Toolset build artifacts *.msi *.wixpdb lemonade-sdk-lemonade-dbde812/.pre-commit-config.yaml000066400000000000000000000005351516551144000226200ustar00rootroot00000000000000exclude: '\.svg$' repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - repo: https://github.com/psf/black rev: 26.1.0 hooks: - id: black language_version: python3 lemonade-sdk-lemonade-dbde812/.signpath/000077500000000000000000000000001516551144000202275ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.signpath/policies/000077500000000000000000000000001516551144000220365ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.signpath/policies/lemonade/000077500000000000000000000000001516551144000236225ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.signpath/policies/lemonade/test-signing.yml000066400000000000000000000003701516551144000267600ustar00rootroot00000000000000# SignPath policy for test-signing # See: https://docs.signpath.io/trusted-build-systems/github github-policies: runners: allowed_groups: - 'GitHub Actions' # GitHub-hosted runners - 'stx' # Ryzen AI self-hosted runners lemonade-sdk-lemonade-dbde812/.ubuntu/000077500000000000000000000000001516551144000177345ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.ubuntu/Dockerfile000066400000000000000000000040151516551144000217260ustar00rootroot00000000000000ARG BASE_IMAGE FROM ${BASE_IMAGE} LABEL org.opencontainers.image.description="Lemonade build environment with all dependencies pre-installed" LABEL org.opencontainers.image.source="https://github.com/lemonade-sdk/lemonade" # Prevent interactive prompts during installation ENV DEBIAN_FRONTEND=noninteractive ARG UBUNTU_PPA="" # Set up PPA if needed RUN if [ -n "$UBUNTU_PPA" ]; then \ apt update && apt install -y software-properties-common && \ add-apt-repository -y "$UBUNTU_PPA"; \ fi # Install all build dependencies COPY contrib /contrib RUN apt update && apt install -y \ build-essential \ curl \ dpkg-dev \ git \ python3-pip \ python3-venv \ unzip \ && \ cd /contrib && \ apt build-dep . -y && \ rm -rf /var/lib/apt/lists/* # Temporary Ubuntu 26.04 workaround for node-commander version RUN UBUNTU_VERSION="$(. /etc/os-release && echo "$VERSION_ID")" && \ if [ "$UBUNTU_VERSION" = "26.04" ]; then \ UBUNTU_CODENAME="$(. /etc/os-release && echo "$UBUNTU_CODENAME")" && \ INSTALLED_NODE_COMMANDER_VERSION="$(dpkg-query -W -f='${Version}' node-commander 2>/dev/null || true)" && \ if [ -z "$INSTALLED_NODE_COMMANDER_VERSION" ] || dpkg --compare-versions "$INSTALLED_NODE_COMMANDER_VERSION" lt "14.0.3-4"; then \ echo "deb http://archive.ubuntu.com/ubuntu ${UBUNTU_CODENAME}-proposed main universe" > /etc/apt/sources.list.d/proposed.list && \ printf "Package: *\nPin: release a=${UBUNTU_CODENAME}-proposed\nPin-Priority: 100\n" > /etc/apt/preferences.d/proposed && \ apt update && \ apt install -y -t "${UBUNTU_CODENAME}-proposed" \ node-commander \ node-babel7-runtime \ node-babel7 \ node-clean-css \ terser \ node-terser && \ rm -f /etc/apt/sources.list.d/proposed.list /etc/apt/preferences.d/proposed; \ fi; \ fi && \ rm -rf /var/lib/apt/lists/* WORKDIR /workspace CMD ["/bin/bash"] lemonade-sdk-lemonade-dbde812/.vscode/000077500000000000000000000000001516551144000176755ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/.vscode/c_cpp_properties.json000066400000000000000000000011271516551144000241310ustar00rootroot00000000000000{ "configurations": [ { "name": "Linux", "compileCommands": "${workspaceFolder}/build/compile_commands.json", "intelliSenseMode": "linux-clang-x64", "cStandard": "c17", "cppStandard": "c++17", "browse": { "path": [ "${workspaceFolder}/src/cpp/include", "${workspaceFolder}/build/include", "${workspaceFolder}/build" ], "limitSymbolsToIncludedHeaders": true } } ], "version": 4 } lemonade-sdk-lemonade-dbde812/.vscode/launch.json000066400000000000000000000012631516551144000220440ustar00rootroot00000000000000{ "version": "0.2.0", "configurations": [ { "name": "C++ Debug", "type": "cppdbg", "request": "launch", // The path to your compiled executable. Change this to match your build output. "program": "${workspaceFolder}/build/lemond", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": false, "MIMode": "gdb", // Configure GDB to work with the debugging session. "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ] } ] } lemonade-sdk-lemonade-dbde812/.vscode/settings.json000066400000000000000000000065301516551144000224340ustar00rootroot00000000000000{ "cmake.sourceDirectory": "${workspaceFolder}", "files.watcherExclude": { "**/dist-app/**": true, "**/build/**": true, "**/app-src/**": true, "**/node_modules/**": true, "**/*.asar": true, "**/win-unpacked/**": true, "**/linux-unpacked/**": true, "**/mac/**": true }, "files.exclude": { "**/build/app-src/**": true, "**/build/app/**": true, "**/*.asar": true }, "search.exclude": { "**/dist-app/**": true, "**/build/**": true, "**/app-src/**": true, "**/node_modules/**": true }, "cursor.exclude": { "**/dist-app/**": true, "**/build/**": true, "**/app-src/**": true, "**/node_modules/**": true, "**/*.asar": true, "**/win-unpacked/**": true, "**/linux-unpacked/**": true, "**/mac/**": true }, "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json", "C_Cpp.intelliSenseEngine": "disabled", "C_Cpp.default.intelliSenseMode": "linux-clang-x64", "clangd.arguments": [ "--compile-commands-dir=build", "--all-scopes-completion", "--background-index", "--clang-tidy", "--header-insertion=iwyu", "--completion-style=detailed", ], "cmake.debugConfig": { "args": [ "--llamacpp", "cpu" ] }, "files.associations": { "*.cpp": "cpp", "any": "cpp", "array": "cpp", "atomic": "cpp", "bit": "cpp", "bitset": "cpp", "cctype": "cpp", "chrono": "cpp", "cinttypes": "cpp", "clocale": "cpp", "cmath": "cpp", "codecvt": "cpp", "compare": "cpp", "complex": "cpp", "concepts": "cpp", "condition_variable": "cpp", "csignal": "cpp", "cstdarg": "cpp", "cstddef": "cpp", "cstdint": "cpp", "cstdio": "cpp", "cstdlib": "cpp", "cstring": "cpp", "ctime": "cpp", "cwchar": "cpp", "cwctype": "cpp", "deque": "cpp", "forward_list": "cpp", "list": "cpp", "map": "cpp", "set": "cpp", "string": "cpp", "unordered_map": "cpp", "unordered_set": "cpp", "vector": "cpp", "exception": "cpp", "algorithm": "cpp", "functional": "cpp", "iterator": "cpp", "memory": "cpp", "memory_resource": "cpp", "numeric": "cpp", "optional": "cpp", "random": "cpp", "ratio": "cpp", "regex": "cpp", "string_view": "cpp", "system_error": "cpp", "tuple": "cpp", "type_traits": "cpp", "utility": "cpp", "fstream": "cpp", "future": "cpp", "initializer_list": "cpp", "iomanip": "cpp", "iosfwd": "cpp", "iostream": "cpp", "istream": "cpp", "limits": "cpp", "mutex": "cpp", "new": "cpp", "numbers": "cpp", "ostream": "cpp", "ranges": "cpp", "semaphore": "cpp", "span": "cpp", "sstream": "cpp", "stdexcept": "cpp", "stop_token": "cpp", "streambuf": "cpp", "thread": "cpp", "typeinfo": "cpp", "valarray": "cpp", "variant": "cpp" }, } lemonade-sdk-lemonade-dbde812/AGENTS.md000066400000000000000000000266301516551144000176460ustar00rootroot00000000000000# AGENTS.md This file provides guidance to agent driven code reviews when working with this repository. ## Project Overview Lemonade is a local LLM server providing GPU and NPU acceleration for running large language models on consumer hardware. It exposes OpenAI-compatible, Ollama-compatible, and Anthropic-compatible REST APIs, plus a WebSocket Realtime API. It supports multiple backends: llama.cpp, FastFlowLM, RyzenAI, whisper.cpp, stable-diffusion.cpp, and Kokoro TTS. ## Architecture ### Executables - **lemond** โ€” Pure HTTP server. Handles REST API, routes requests to backends, manages model loading/unloading. Configured via `config.json` in the lemonade cache directory. CLI args: `[cache_dir] [--port PORT] [--host HOST]`. - **lemonade** โ€” CLI client (`src/cpp/cli/`). Commands: `list`, `pull`, `delete`, `run`, `status`, `logs`, `launch`, `recipes`, `scan`, etc. Communicates with router via HTTP. Discovers running server via UDP beacon. - **LemonadeServer.exe** (Windows) โ€” SUBSYSTEM:WINDOWS GUI app that embeds `lemond` and shows a system tray icon. Auto-starts via Windows startup folder. - **lemonade-tray** (macOS/Linux) โ€” Lightweight tray client that connects to a running `lemond`. Platform code in `src/cpp/tray/platform/`. - **lemonade-server** โ€” Deprecated backwards-compatibility shim. Delegates to `lemond` or `lemonade`. ### Backend Abstraction `WrappedServer` (`src/cpp/include/lemon/wrapped_server.h`) is the abstract base class. Each backend inherits it and implements `load()`, `unload()`, `chat_completion()`, `completion()`, `responses()`, and optionally `install()` / `download_model()`. Backends run as **subprocesses** โ€” Lemonade forwards HTTP requests to them. | Backend | Class | Capabilities | Device | Purpose | |---------|-------|-------------|--------|---------| | llama.cpp | `LlamaCppServer` | Completion, Embeddings, Reranking | GPU | LLM inference โ€” CPU/GPU (Vulkan, ROCm, Metal) | | FastFlowLM | `FastFlowLMServer` | Completion, Embeddings, Reranking, Audio | NPU | NPU inference (multi-modal: LLM, ASR, embeddings, reranking) | | RyzenAI | `RyzenAIServer` | Completion | NPU | Hybrid NPU inference | | whisper.cpp | `WhisperServer` | Audio | CPU | Audio transcription | | stable-diffusion.cpp | `SdServer` | Image | CPU | Image generation, editing, variations | | Kokoro | `KokoroServer` | TTS | CPU | Text-to-speech | Capability interfaces: `ICompletionServer`, `IEmbeddingsServer`, `IRerankingServer`, `IAudioServer`, `IImageServer`, `ITextToSpeechServer` (defined in `server_capabilities.h`). Use `supports_capability(server)` template for runtime checks. ### Router & Multi-Model Support `Router` (`src/cpp/server/router.cpp`) manages a vector of `WrappedServer` instances. Routes requests based on model recipe, maintains LRU caches per model type (LLM, embedding, reranking, audio, image, TTS โ€” see `model_types.h`), and enforces NPU exclusivity. Configurable via `--max-loaded-models`. On non-file-not-found errors, the router uses a "nuclear option" โ€” evicts all models and retries the load. ### Model Manager & Recipe System `ModelManager` (`src/cpp/server/model_manager.cpp`) loads the registry from `src/cpp/resources/server_models.json`. Each model has "recipes" defining which backend and config to use. Backend versions are pinned in `src/cpp/resources/backend_versions.json`. Models download from Hugging Face. ### API Routes All core endpoints are registered under **4 path prefixes**: - `/api/v0/` โ€” Legacy - `/api/v1/` โ€” Current - `/v0/` โ€” Legacy short - `/v1/` โ€” OpenAI SDK / LiteLLM compatibility **Core endpoints:** `chat/completions`, `completions`, `embeddings`, `reranking`, `models`, `models/{id}`, `health`, `pull`, `load`, `unload`, `delete`, `params`, `install`, `uninstall`, `audio/transcriptions`, `audio/speech`, `images/generations`, `images/edits`, `images/variations`, `responses`, `stats`, `system-info`, `system-stats`, `log-level`, `logs/stream` **Ollama-compatible endpoints** (under `/api/` without version prefix): `chat`, `generate`, `tags`, `show`, `delete`, `pull`, `embed`, `embeddings`, `ps`, `version` **Anthropic-compatible endpoint:** `POST /api/messages` โ€” supports message completion, tool use, and SSE streaming. **WebSocket Realtime API**: OpenAI-compatible Realtime protocol for real-time audio transcription. Binds to an OS-assigned port (9000+), exposed via the `websocket_port` field in the `/health` endpoint response. **Internal endpoints:** `POST /internal/shutdown` Optional API key auth via `LEMONADE_API_KEY` env var (regular API endpoints) or `LEMONADE_ADMIN_API_KEY` env var (full access including internal endpoints). Clients prefer `LEMONADE_ADMIN_API_KEY` if set. CORS enabled on all routes. ### Desktop & Web App - **Electron app** โ€” React 19 + TypeScript in `src/app/`. Pure CSS (dark theme), context-based state. Key components: `ChatWindow.tsx`, `ModelManager.tsx`, `DownloadManager.tsx`, `BackendManager.tsx`. Feature panels: LLMChat, ImageGeneration, Transcription, TTS, Embedding, Reranking. - **Web app** โ€” Browser-only version in `src/web-app/`. Symlinks source from `src/app/src/`. Built via CMake `BUILD_WEB_APP=ON`. Served at `/app`. ### Key Dependencies **C++ (FetchContent):** cpp-httplib, nlohmann/json, CLI11, libcurl, zstd, libwebsockets, brotli (macOS). Platform SSL: Schannel (Windows), SecureTransport (macOS), OpenSSL (Linux). **Electron:** React 19, TypeScript 5.3, Webpack 5, Electron 39, markdown-it, highlight.js, katex. ## Build Commands CMakeLists.txt is at the repository root. Build uses CMake presets โ€” run the setup script first, then build with `--preset`. ```bash # 1. Setup (configures build directory and installs deps) ./setup.sh # Linux / macOS ./setup.ps1 # Windows (PowerShell) # 2. Build C++ server cmake --build --preset default # Linux / macOS (Ninja) cmake --build --preset windows # Windows (Visual Studio 2022) cmake --build --preset vs18 # Windows (Visual Studio 2026) # 3. Electron app (optional, requires Node.js 20+) cmake --build --preset default --target electron-app # Linux / macOS cmake --build --preset windows --target electron-app # Windows (VS 2022) cmake --build --preset vs18 --target electron-app # Windows (VS 2026) # 4. Web app (auto-built on non-Windows; manual on Windows) cmake --build --preset default --target web-app # Linux / macOS cmake --build --preset windows --target web-app # Windows # 5. Windows MSI installer (WiX 5.0+ required) cmake --build --preset windows --target wix_installer_minimal # server + web-app cmake --build --preset windows --target wix_installer_full # server + electron + web-app # 6. macOS signed installer cmake --build --preset default --target package-macos # 7. Linux .deb / .rpm cd build && cpack # .deb cd build && cpack -G RPM # .rpm # 8. Linux AppImage cmake --build --preset default --target appimage ``` CMake presets: `default` (Ninja, Release), `windows` (VS 2022), `vs18` (VS 2026), `debug` (Ninja, Debug). CMake options: `BUILD_WEB_APP` (ON by default on non-Windows), `BUILD_ELECTRON_APP` (Linux only, include Electron in deb), `LEMONADE_SYSTEMD_UNIT_NAME` (default: `lemonade-server.service`). ## Testing Integration tests in Python against a live server. Tests auto-discover the server binary from the build directory; use `--server-binary` to override. ```bash pip install -r test/requirements.txt # CLI tests (no inference backend needed) python test/server_cli.py # Endpoint tests (no inference backend needed) python test/server_endpoints.py # LLM tests (specify wrapped server and backend) python test/server_llm.py --wrapped-server llamacpp --backend vulkan # Audio transcription tests python test/server_whisper.py # Image generation tests (slow) python test/server_sd.py ``` Test utilities in `test/utils/` with `server_base.py` as the base class. Test dependencies include `requests`, `httpx`, `openai`, `huggingface_hub`, `psutil`, `numpy`, `websockets`, and `ollama`. ## Code Style ### C++ - C++17, `lemon::` namespace - `snake_case` for functions/variables, `CamelCase` for classes/types - 4-space indent, `#pragma once` for headers - Keep `#include` directives in alphabetical order within each include block - Platform guards: `#ifdef _WIN32`, `#ifdef __APPLE__`, `#ifdef __linux__` ### Python - **Black** formatting (v26.1.0, enforced in CI) - Pylint with `.pylintrc` - Pre-commit hooks: trailing-whitespace, end-of-file-fixer, check-yaml, check-added-large-files ### TypeScript/React - React 19, pure CSS (dark theme), context-based state - UI/frontend changes are handled by core maintainers only ## Key Files | File | Purpose | |------|---------| | `CMakeLists.txt` | Root build config (version, deps, targets) | | `src/cpp/server/server.cpp` | HTTP route registration and all handlers | | `src/cpp/server/router.cpp` | Request routing and multi-model orchestration | | `src/cpp/server/model_manager.cpp` | Model registry, downloads, recipe resolution | | `src/cpp/include/lemon/wrapped_server.h` | Backend abstract base class | | `src/cpp/include/lemon/server_capabilities.h` | Backend capability interfaces | | `src/cpp/resources/server_models.json` | Model registry | | `src/cpp/resources/backend_versions.json` | Backend version pins | | `src/cpp/server/anthropic_api.cpp` | Anthropic API compatibility | | `src/cpp/server/ollama_api.cpp` | Ollama API compatibility | | `src/cpp/include/lemon/websocket_server.h` | WebSocket Realtime API server | | `src/cpp/include/lemon/model_types.h` | Model type and device type enums | | `src/cpp/include/lemon/config_file.h` | config.json load/save/migrate | | `src/cpp/include/lemon/recipe_options.h` | Per-recipe JSON configuration | | `src/cpp/tray/tray_app.cpp` | Tray application UI and logic | | `src/app/src/renderer/ModelManager.tsx` | Model management UI | | `src/app/src/renderer/ChatWindow.tsx` | Chat interface | ## Critical Invariants These MUST be maintained in all changes: 1. **Quad-prefix registration** โ€” Every new endpoint MUST be registered under `/api/v0/`, `/api/v1/`, `/v0/`, AND `/v1/`. 2. **NPU exclusivity** โ€” Exclusive-NPU recipes (`ryzenai-llm`, `whispercpp` on NPU) evict ALL other NPU models before loading. FastFlowLM (`flm`) can coexist with other FLM types (max 1 per FLM type) but not with exclusive-NPU recipes. 3. **WrappedServer contract** โ€” New backends MUST implement all core virtual methods: `load()`, `unload()`, `chat_completion()`, `completion()`, `responses()`. 4. **Subprocess model** โ€” Backends run as subprocesses (llama-server, whisper-server, sd-server, koko, flm, ryzenai-server). They must NOT run in-process. 5. **Recipe integrity** โ€” Changes to `server_models.json` must have valid recipes referencing backends in `backend_versions.json`. 6. **Cross-platform** โ€” Code must compile on Windows (MSVC), Linux (GCC/Clang), macOS (AppleClang). Platform-specific code must use `#ifdef` guards. 7. **No hardcoded paths** โ€” Use path utilities. Windows/Linux/macOS paths differ. 8. **Thread safety** โ€” Router serves concurrent HTTP requests. Shared state must be properly guarded. 9. **Ollama compatibility** โ€” Changes to model listing or management must not break `/api/*` Ollama endpoints. 10. **API key passthrough** โ€” When `LEMONADE_API_KEY` is set, all API routes must enforce authentication. ## Contributing - Open an Issue before submitting major PRs - UI/frontend changes are handled by core maintainers only - Python formatting with Black is required - PRs trigger CI for linting, formatting, and integration tests lemonade-sdk-lemonade-dbde812/CLAUDE.md000066400000000000000000000000131516551144000176050ustar00rootroot00000000000000@AGENTS.md lemonade-sdk-lemonade-dbde812/CMakeLists.txt000066400000000000000000002103471516551144000211030ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.12...3.28) # Use static runtime library on Windows to avoid DLL dependencies if(MSVC) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") endif() project(lemon_cpp VERSION 10.2.0) # Export compile_commands.json for clangd/IntelliSense set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Enable Objective-C++ for macOS Metal support if(APPLE) enable_language(OBJCXX) endif() # Prevent finding Homebrew libraries on macOS to use system ones if(APPLE) set(CMAKE_IGNORE_PATH "/opt/homebrew/lib" "/opt/homebrew/include") endif() # Set default install prefix to /opt on Linux if not specified if(UNIX AND NOT APPLE AND CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "/opt" CACHE PATH "Install prefix" FORCE) endif() # Include GNUInstallDirs for standard installation directories include(GNUInstallDirs) # Install man pages install(FILES ${CMAKE_SOURCE_DIR}/docs/man/man1/lemonade-server.1 ${CMAKE_SOURCE_DIR}/docs/man/man1/lemond.1 ${CMAKE_SOURCE_DIR}/docs/man/man1/lemonade.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 ) # ============================================================ # Electron source paths (used by both runtime and installers) # ============================================================ get_filename_component(ELECTRON_APP_SOURCE_DIR "${CMAKE_SOURCE_DIR}/src/app" ABSOLUTE) set(ELECTRON_APP_BUILD_DIR "${CMAKE_BINARY_DIR}/app") if(WIN32) set(ELECTRON_APP_UNPACKED_DIR "${ELECTRON_APP_BUILD_DIR}/win-unpacked") set(ELECTRON_EXE_NAME "lemonade-app.exe") elseif(APPLE) set(ELECTRON_APP_UNPACKED_DIR "${ELECTRON_APP_BUILD_DIR}/mac-arm64") set(ELECTRON_EXE_NAME "lemonade-app.app") else() set(ELECTRON_APP_UNPACKED_DIR "${ELECTRON_APP_BUILD_DIR}/linux-unpacked") set(ELECTRON_EXE_NAME "lemonade-app") endif() # ============================================================ # Web App source paths # ============================================================ get_filename_component(WEB_APP_SOURCE_DIR "${CMAKE_SOURCE_DIR}/src/web-app" ABSOLUTE) set(WEB_APP_BUILD_DIR "${CMAKE_BINARY_DIR}/resources/web-app") # When ON, use system-provided Node.js modules (for example, webpack and distro-packaged JS deps) # instead of installing project-local dependencies with npm for web-app builds. option(USE_SYSTEM_NODEJS_MODULES "Use system-installed Node.js modules for building the Web app" OFF) # C++ Standard set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Try system packages first, fall back to FetchContent if not available # Define minimum version requirements based on FetchContent git tags set(MIN_NLOHMANN_JSON_VERSION "3.11.3") set(MIN_CLI11_VERSION "2.4.2") set(MIN_CURL_VERSION "8.5.0") set(MIN_ZSTD_VERSION "1.5.5") set(DOWNLOAD_ZSTD_VERSION "1.5.7") set(MIN_HTTPLIB_VERSION "0.26.0") set(MIN_LWS_VERSION "4.3.3") find_package(PkgConfig QUIET) # Try to find system packages with version requirements find_package(nlohmann_json ${MIN_NLOHMANN_JSON_VERSION} QUIET) if(PkgConfig_FOUND) pkg_check_modules(CURL QUIET libcurl>=${MIN_CURL_VERSION}) pkg_check_modules(ZSTD QUIET libzstd>=${MIN_ZSTD_VERSION}) pkg_check_modules(HTTPLIB QUIET cpp-httplib>=${MIN_HTTPLIB_VERSION}) pkg_check_modules(LWS QUIET libwebsockets>=${MIN_LWS_VERSION}) endif() find_path(CLI11_INCLUDE_DIRS "CLI/CLI.hpp") find_path(HTTPLIB_INCLUDE_DIRS "httplib.h") # Verify CLI11 version if found if(CLI11_INCLUDE_DIRS) if(EXISTS "${CLI11_INCLUDE_DIRS}/CLI/Version.hpp") file(STRINGS "${CLI11_INCLUDE_DIRS}/CLI/Version.hpp" CLI11_VERSION_LINE REGEX "^#define CLI11_VERSION ") if(CLI11_VERSION_LINE) string(REGEX REPLACE "^#define CLI11_VERSION \"([0-9]+\\.[0-9]+\\.[0-9]+)\"" "\\1" CLI11_VERSION "${CLI11_VERSION_LINE}") if(CLI11_VERSION VERSION_LESS MIN_CLI11_VERSION) message(STATUS "System CLI11 version ${CLI11_VERSION} is less than required ${MIN_CLI11_VERSION}") unset(CLI11_INCLUDE_DIRS) endif() endif() endif() endif() # Determine which dependencies need to be fetched set(USE_SYSTEM_JSON ${nlohmann_json_FOUND}) set(USE_SYSTEM_CURL ${CURL_FOUND}) set(USE_SYSTEM_ZSTD ${ZSTD_FOUND}) set(USE_SYSTEM_CLI11 ${CLI11_INCLUDE_DIRS}) set(USE_SYSTEM_HTTPLIB ${HTTPLIB_FOUND}) set(USE_SYSTEM_LWS ${LWS_FOUND}) # On macOS, always statically link dependencies to avoid Homebrew dylib issues if(APPLE) set(USE_SYSTEM_ZSTD OFF) set(USE_SYSTEM_CURL OFF) set(USE_SYSTEM_HTTPLIB OFF) set(USE_SYSTEM_LWS OFF) endif() if(USE_SYSTEM_JSON) message(STATUS "Using system nlohmann_json (>= ${MIN_NLOHMANN_JSON_VERSION})") else() message(STATUS "System nlohmann_json not found or version < ${MIN_NLOHMANN_JSON_VERSION}, will use FetchContent") endif() if(USE_SYSTEM_CURL) message(STATUS "Using system libcurl (version ${CURL_VERSION}, >= ${MIN_CURL_VERSION})") else() message(STATUS "System libcurl not found or version < ${MIN_CURL_VERSION}, will use FetchContent") endif() if(USE_SYSTEM_ZSTD) message(STATUS "Using system zstd (version ${ZSTD_VERSION}, >= ${MIN_ZSTD_VERSION})") else() message(STATUS "System zstd not found or version < ${MIN_ZSTD_VERSION}, will use FetchContent") endif() if(USE_SYSTEM_CLI11) if(CLI11_VERSION) message(STATUS "Using system CLI11 (version ${CLI11_VERSION}, >= ${MIN_CLI11_VERSION})") else() message(STATUS "Using system CLI11 (>= ${MIN_CLI11_VERSION})") endif() else() message(STATUS "System CLI11 not found or version < ${MIN_CLI11_VERSION}, will use FetchContent") endif() if(USE_SYSTEM_HTTPLIB) if(HTTPLIB_VERSION) message(STATUS "Using system cpp-httplib (version ${HTTPLIB_VERSION}, >= ${MIN_HTTPLIB_VERSION})") else() message(STATUS "Using system cpp-httplib (>= ${MIN_HTTPLIB_VERSION})") endif() else() message(STATUS "System cpp-httplib not found or version < ${MIN_HTTPLIB_VERSION}, will use FetchContent") endif() if(USE_SYSTEM_LWS) if(LWS_VERSION) message(STATUS "Using system libwebsockets (version ${LWS_VERSION}, >= ${MIN_LWS_VERSION})") else() message(STATUS "Using system libwebsockets (>= ${MIN_LWS_VERSION})") endif() else() message(STATUS "System libwebsockets not found or version < ${MIN_LWS_VERSION}, will use FetchContent") endif() # ============================================================ # Common dependency configurations # ============================================================ # Common configurations for all dependencies set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "") # Build static libraries # Link to MSVC library statically set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") # Fetch missing dependencies if(NOT USE_SYSTEM_JSON OR NOT USE_SYSTEM_CURL OR NOT USE_SYSTEM_ZSTD OR NOT USE_SYSTEM_CLI11 OR NOT USE_SYSTEM_HTTPLIB OR NOT USE_SYSTEM_LWS) include(FetchContent) endif() if(APPLE) # brotli (MIT License) - required by httplib for compression on macOS FetchContent_Declare(brotli GIT_REPOSITORY https://github.com/google/brotli.git GIT_TAG v1.1.0 GIT_SHALLOW TRUE EXCLUDE_FROM_ALL ) endif() # === nlohmann/json === if(NOT USE_SYSTEM_JSON) FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v${MIN_NLOHMANN_JSON_VERSION} GIT_SHALLOW TRUE ) set(JSON_BuildTests OFF CACHE INTERNAL "") set(JSON_Install OFF CACHE INTERNAL "") FetchContent_MakeAvailable(json) endif() # === CLI11 === if(NOT USE_SYSTEM_CLI11) FetchContent_Declare(CLI11 GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git GIT_TAG v${MIN_CLI11_VERSION} GIT_SHALLOW TRUE ) set(CLI11_BUILD_TESTS OFF CACHE INTERNAL "") set(CLI11_BUILD_EXAMPLES OFF CACHE INTERNAL "") FetchContent_MakeAvailable(CLI11) endif() # === curl === if(NOT USE_SYSTEM_CURL) string(REPLACE "." "_" CURL_TAG_VERSION "${MIN_CURL_VERSION}") FetchContent_Declare(curl GIT_REPOSITORY https://github.com/curl/curl.git GIT_TAG curl-${CURL_TAG_VERSION} GIT_SHALLOW TRUE EXCLUDE_FROM_ALL ) set(BUILD_STATIC_LIBS ON CACHE INTERNAL "") set(BUILD_CURL_EXE OFF CACHE INTERNAL "") set(CURL_DISABLE_TESTS ON CACHE INTERNAL "") set(CURL_DISABLE_INSTALL ON CACHE INTERNAL "") set(HTTP_ONLY ON CACHE INTERNAL "") if(WIN32) set(CURL_STATIC_CRT ON CACHE INTERNAL "") set(CURL_USE_SCHANNEL ON CACHE INTERNAL "") set(CMAKE_USE_SCHANNEL ON CACHE INTERNAL "") elseif(APPLE) set(CURL_USE_SECTRANSP ON CACHE INTERNAL "") else() set(CURL_USE_OPENSSL ON CACHE INTERNAL "") endif() FetchContent_MakeAvailable(curl) endif() # === zstd === if(NOT USE_SYSTEM_ZSTD) FetchContent_Declare(zstd GIT_REPOSITORY https://github.com/facebook/zstd.git GIT_TAG v${DOWNLOAD_ZSTD_VERSION} GIT_SHALLOW TRUE SOURCE_SUBDIR build/cmake EXCLUDE_FROM_ALL ) set(ZSTD_BUILD_STATIC ON CACHE INTERNAL "") set(ZSTD_BUILD_PROGRAMS OFF CACHE INTERNAL "") set(ZSTD_BUILD_SHARED OFF CACHE INTERNAL "") set(ZSTD_BUILD_TESTS OFF CACHE INTERNAL "") FetchContent_MakeAvailable(zstd) add_library(zstd::libzstd ALIAS libzstd_static) else() # Create an imported target for system zstd so httplib can find it if(NOT TARGET zstd::libzstd) add_library(zstd::libzstd UNKNOWN IMPORTED) # Find the actual library file find_library(ZSTD_LIBRARY NAMES zstd HINTS ${ZSTD_LIBRARY_DIRS}) if(ZSTD_LIBRARY) set_target_properties(zstd::libzstd PROPERTIES IMPORTED_LOCATION "${ZSTD_LIBRARY}" IMPORTED_LOCATION_RELEASE "${ZSTD_LIBRARY}" IMPORTED_LOCATION_DEBUG "${ZSTD_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${ZSTD_INCLUDE_DIRS}" ) endif() endif() endif() if(APPLE) # === brotli === set(BROTLI_BUILD_STATIC ON CACHE INTERNAL "") set(BROTLI_BUILD_PROGRAMS OFF CACHE INTERNAL "") set(BROTLI_BUILD_SHARED OFF CACHE INTERNAL "") set(BROTLI_BUILD_TESTS OFF CACHE INTERNAL "") FetchContent_MakeAvailable(brotli) if(TARGET brotlicommon) add_library(Brotli::common ALIAS brotlicommon) endif() if(TARGET brotlienc) add_library(Brotli::encoder ALIAS brotlienc) endif() if(TARGET brotlidec) add_library(Brotli::decoder ALIAS brotlidec) endif() endif() # === httplib === if(NOT USE_SYSTEM_HTTPLIB) FetchContent_Declare(httplib GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git GIT_TAG v${MIN_HTTPLIB_VERSION} GIT_SHALLOW TRUE ) set(HTTPLIB_REQUIRE_OPENSSL OFF CACHE INTERNAL "") set(HTTPLIB_USE_OPENSSL_IF_AVAILABLE OFF CACHE INTERNAL "") set(HTTPLIB_REQUIRE_ZSTD OFF CACHE INTERNAL "") set(HTTPLIB_USE_ZSTD_IF_AVAILABLE OFF CACHE INTERNAL "") set(HTTPLIB_IS_USING_ZSTD ON CACHE INTERNAL "") set(HTTPLIB_INSTALL OFF CACHE INTERNAL "") FetchContent_MakeAvailable(httplib) if(NOT USE_SYSTEM_ZSTD) target_include_directories(httplib INTERFACE ${zstd_SOURCE_DIR}/lib ${zstd_SOURCE_DIR}/lib/common ) endif() endif() # === libwebsockets (for WebSocket support) === if(NOT USE_SYSTEM_LWS) # Workaround: libwebsockets v4.3.3 uses cmake_minimum_required(VERSION 2.8.12) # which CMake 4.x rejects. Allow older policy versions for subdirectory builds. if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.27") cmake_policy(SET CMP0148 OLD) endif() set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE) FetchContent_Declare(libwebsockets GIT_REPOSITORY https://github.com/warmcat/libwebsockets.git GIT_TAG v${MIN_LWS_VERSION} GIT_SHALLOW TRUE ) # Build as static library, disable features we don't need set(LWS_WITH_SHARED OFF CACHE BOOL "" FORCE) set(LWS_WITHOUT_TESTAPPS ON CACHE BOOL "" FORCE) set(LWS_WITHOUT_TEST_SERVER ON CACHE BOOL "" FORCE) set(LWS_WITHOUT_TEST_PING ON CACHE BOOL "" FORCE) set(LWS_WITHOUT_TEST_CLIENT ON CACHE BOOL "" FORCE) set(LWS_WITH_MINIMAL_EXAMPLES OFF CACHE BOOL "" FORCE) # Disable TLS for now (can be enabled later for WSS/network access) set(LWS_WITH_SSL OFF CACHE BOOL "" FORCE) set(LWS_WITH_ZLIB OFF CACHE BOOL "" FORCE) set(LWS_WITH_ZIP_FOPS OFF CACHE BOOL "" FORCE) # Disable -Werror for libwebsockets โ€” newer GCC/Clang triggers warnings in upstream code # First populate to get the source FetchContent_Populate(libwebsockets) # Patch the CMakeLists.txt files to remove -Werror file(READ "${libwebsockets_SOURCE_DIR}/CMakeLists.txt" LWS_CMAKE_MAIN) string(REPLACE "-Werror" "" LWS_CMAKE_MAIN "${LWS_CMAKE_MAIN}") file(WRITE "${libwebsockets_SOURCE_DIR}/CMakeLists.txt" "${LWS_CMAKE_MAIN}") file(READ "${libwebsockets_SOURCE_DIR}/lib/CMakeLists.txt" LWS_CMAKE_LIB) string(REPLACE "-Werror" "" LWS_CMAKE_LIB "${LWS_CMAKE_LIB}") file(WRITE "${libwebsockets_SOURCE_DIR}/lib/CMakeLists.txt" "${LWS_CMAKE_LIB}") # Now add the subdirectory add_subdirectory(${libwebsockets_SOURCE_DIR} ${libwebsockets_BINARY_DIR}) message(STATUS "libwebsockets will be fetched (v${MIN_LWS_VERSION})") endif() # Use brotli for compression on macOS if(APPLE) set(HTTPLIB_REQUIRE_BROTLI OFF CACHE INTERNAL "") # Skip brotli checks set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF CACHE INTERNAL "") # Skip brotli checks set(HTTPLIB_IS_USING_BROTLI ON CACHE INTERNAL "") # brotli is provided endif() # ============================================================ # Application: lemond # ============================================================ set(EXECUTABLE_NAME "lemond") # ============================================================ # Compilation configurations # ============================================================ # Common compiler definitions # Enable httplib thread pool with 8 threads add_compile_definitions(CPPHTTPLIB_THREAD_POOL_COUNT=8) # Platform-specific compiler definitions if(WIN32) # Set Windows target version to Windows 10 for httplib v0.26.0 add_compile_definitions(_WIN32_WINNT=0x0A00) if(MSVC) # Add security-hardening compiler flags for MSVC # Control Flow Guard - prevents control flow hijacking add_compile_options(/guard:cf) # Buffer Security Check - stack buffer overflow detection add_compile_options(/GS) endif() endif() # ============================================================ # Compilation # ============================================================ # Generate version header from template configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/include/lemon/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/include/lemon/version.h @ONLY ) # Generate manifest files from templates (Windows only) if(WIN32) configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/server/lemonade.manifest.in ${CMAKE_CURRENT_BINARY_DIR}/server/lemonade.manifest @ONLY ) # ============================================================ # WiX Toolset Configuration (Windows MSI Installer) # ============================================================ # Configure WiX Product.wxs template with version configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/installer/Product.wxs.in ${CMAKE_CURRENT_BINARY_DIR}/installer/Product.wxs @ONLY ) set(WIX_PRODUCT_WXS "${CMAKE_CURRENT_BINARY_DIR}/installer/Product.wxs") set(WIX_FRAGMENT_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/installer/generate_electron_fragment.py") # Find WiX Toolset 5.0+ (unified 'wix' command) and Python for fragment generation find_program(WIX_EXECUTABLE wix) find_package(Python3 COMPONENTS Interpreter) if(WIX_EXECUTABLE) execute_process( COMMAND ${WIX_EXECUTABLE} --version OUTPUT_VARIABLE WIX_VERSION_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE ) message(STATUS "WiX Toolset found: ${WIX_EXECUTABLE}") message(STATUS "WiX version: ${WIX_VERSION_OUTPUT}") file(TO_NATIVE_PATH "${CMAKE_CURRENT_SOURCE_DIR}" WIX_SOURCE_DIR_NATIVE) file(TO_NATIVE_PATH "${WIX_PRODUCT_WXS}" WIX_PRODUCT_WXS_NATIVE) file(TO_NATIVE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/lemonade-server-minimal.msi" WIX_MINIMAL_OUTPUT_NATIVE) file(TO_NATIVE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/lemonade.msi" WIX_FULL_OUTPUT_NATIVE) file(TO_NATIVE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/src/cpp" WIX_CPP_SOURCE_DIR_NATIVE) file(TO_NATIVE_PATH "${CMAKE_BINARY_DIR}" WIX_BUILD_DIR_NATIVE) file(TO_NATIVE_PATH "${ELECTRON_APP_UNPACKED_DIR}" WIX_ELECTRON_SOURCE_NATIVE) set(WIX_ELECTRON_FRAGMENT "${CMAKE_CURRENT_BINARY_DIR}/installer/ElectronAppFragment.wxs") file(TO_NATIVE_PATH "${WIX_ELECTRON_FRAGMENT}" WIX_ELECTRON_FRAGMENT_NATIVE) # Web app fragment paths file(TO_NATIVE_PATH "${WEB_APP_BUILD_DIR}" WIX_WEBAPP_SOURCE_NATIVE) set(WIX_WEBAPP_FRAGMENT "${CMAKE_CURRENT_BINARY_DIR}/installer/WebAppFragment.wxs") file(TO_NATIVE_PATH "${WIX_WEBAPP_FRAGMENT}" WIX_WEBAPP_FRAGMENT_NATIVE) # Both installers require Python3 for fragment generation and always include the web app if(NOT Python3_FOUND) message(FATAL_ERROR "Python 3 is required for WiX installer fragment generation. Install Python 3 and ensure it is in PATH.") endif() # Minimal installer (server + web-app) add_custom_target(wix_installer_minimal COMMAND ${CMAKE_COMMAND} -E echo "Building WiX MSI installer (server + web-app)..." # Generate web-app fragment COMMAND ${Python3_EXECUTABLE} ${WIX_FRAGMENT_SCRIPT} --source "${WEB_APP_BUILD_DIR}" --output "${WIX_WEBAPP_FRAGMENT}" --component-group "WebAppComponents" --root-id "WebAppDir" --path-variable "WebAppSourceDir" COMMAND ${WIX_EXECUTABLE} build -arch x64 -ext WixToolset.UI.wixext -d SourceDir="${WIX_SOURCE_DIR_NATIVE}" -d CppSourceDir="${WIX_CPP_SOURCE_DIR_NATIVE}" -d BuildDir="${WIX_BUILD_DIR_NATIVE}" -d IncludeElectron=0 -d IncludeWebApp=1 -d WebAppSourceDir="${WIX_WEBAPP_SOURCE_NATIVE}" -out "${WIX_MINIMAL_OUTPUT_NATIVE}" "${WIX_PRODUCT_WXS_NATIVE}" "${WIX_WEBAPP_FRAGMENT_NATIVE}" COMMAND ${CMAKE_COMMAND} -E echo "MSI installer created: lemonade-server-minimal.msi" DEPENDS ${EXECUTABLE_NAME} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Building WiX MSI installer (server + web-app)" ) set(WIX_INSTALLER_TARGETS wix_installer_minimal) # Full installer (server + Electron app + web-app) add_custom_target(wix_installer_full COMMAND ${CMAKE_COMMAND} -E echo "Building WiX MSI installer (with Electron app)..." # Generate web-app fragment COMMAND ${Python3_EXECUTABLE} ${WIX_FRAGMENT_SCRIPT} --source "${WEB_APP_BUILD_DIR}" --output "${WIX_WEBAPP_FRAGMENT}" --component-group "WebAppComponents" --root-id "WebAppDir" --path-variable "WebAppSourceDir" # Generate Electron app fragment COMMAND ${Python3_EXECUTABLE} ${WIX_FRAGMENT_SCRIPT} --source "${ELECTRON_APP_UNPACKED_DIR}" --output "${WIX_ELECTRON_FRAGMENT}" --component-group "ElectronAppComponents" --root-id "ElectronAppDir" --path-variable "ElectronSourceDir" COMMAND ${WIX_EXECUTABLE} build -arch x64 -ext WixToolset.UI.wixext -d SourceDir="${WIX_SOURCE_DIR_NATIVE}" -d CppSourceDir="${WIX_CPP_SOURCE_DIR_NATIVE}" -d BuildDir="${WIX_BUILD_DIR_NATIVE}" -d IncludeElectron=1 -d IncludeWebApp=1 -d ElectronSourceDir="${WIX_ELECTRON_SOURCE_NATIVE}" -d WebAppSourceDir="${WIX_WEBAPP_SOURCE_NATIVE}" -out "${WIX_FULL_OUTPUT_NATIVE}" "${WIX_PRODUCT_WXS_NATIVE}" "${WIX_ELECTRON_FRAGMENT_NATIVE}" "${WIX_WEBAPP_FRAGMENT_NATIVE}" COMMAND ${CMAKE_COMMAND} -E echo "MSI installer created: lemonade.msi" DEPENDS ${EXECUTABLE_NAME} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Building WiX MSI installer (with Electron app)" ) list(APPEND WIX_INSTALLER_TARGETS wix_installer_full) add_custom_target(wix_installers DEPENDS ${WIX_INSTALLER_TARGETS} ) message(STATUS "WiX installer targets configured. Run 'cmake --build . --target wix_installer_minimal' or 'wix_installer_full'.") else() message(STATUS "WiX Toolset not found. MSI installers will not be available.") message(STATUS " Install WiX Toolset 5.0.2 from: https://github.com/wixtoolset/wix/releases/download/v5.0.2/wix-cli-x64.msi") message(STATUS " Or visit: https://wixtoolset.org/") endif() endif() # Include directories include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/include ${CMAKE_CURRENT_BINARY_DIR}/include ${CMAKE_CURRENT_BINARY_DIR} ) # ============================================================ # Server core sources (everything except entry points) # ============================================================ set(SOURCES_CORE src/cpp/server/server.cpp src/cpp/server/router.cpp src/cpp/server/cli_parser.cpp src/cpp/server/config_file.cpp src/cpp/server/model_manager.cpp src/cpp/server/hf_variants.cpp src/cpp/server/wrapped_server.cpp src/cpp/server/streaming_proxy.cpp src/cpp/server/system_info.cpp src/cpp/server/recipe_options.cpp src/cpp/server/runtime_config.cpp src/cpp/server/logging_config.cpp src/cpp/server/log_stream.cpp src/cpp/server/utils/http_client.cpp src/cpp/server/utils/json_utils.cpp src/cpp/server/utils/process_manager.cpp src/cpp/server/utils/path_utils.cpp src/cpp/server/utils/version_utils.cpp src/cpp/server/utils/wmi_helper.cpp src/cpp/server/utils/network_beacon.cpp src/cpp/server/backends/llamacpp_server.cpp src/cpp/server/backends/fastflowlm_server.cpp src/cpp/server/backends/ryzenaiserver.cpp src/cpp/server/backends/whisper_server.cpp src/cpp/server/backends/kokoro_server.cpp src/cpp/server/backends/sd_server.cpp src/cpp/server/backends/backend_utils.cpp src/cpp/server/backend_manager.cpp src/cpp/server/ollama_api.cpp src/cpp/server/anthropic_api.cpp src/cpp/server/streaming_audio_buffer.cpp src/cpp/server/vad.cpp src/cpp/server/realtime_session.cpp src/cpp/server/websocket_server.cpp ) # Add platform-specific source files to core if(APPLE) list(APPEND SOURCES_CORE src/cpp/server/macos_system_info.mm) set_source_files_properties(src/cpp/server/macos_system_info.mm PROPERTIES LANGUAGE OBJCXX) endif() # ============================================================ # Server core OBJECT library (shared by lemond and Lemonade.exe) # ============================================================ add_library(lemonade-server-core OBJECT ${SOURCES_CORE}) # ============================================================ # lemond executable # ============================================================ add_executable(${EXECUTABLE_NAME} src/cpp/server/main.cpp) if(WIN32) target_sources(${EXECUTABLE_NAME} PRIVATE src/cpp/server/version.rc) endif() # ============================================================ # Linking configurations # ============================================================ # Platform-specific linking options if(WIN32) if(MSVC) # Linker security flags add_link_options( /DYNAMICBASE # Address Space Layout Randomization (ASLR) /NXCOMPAT # Data Execution Prevention (DEP) /GUARD:CF # Control Flow Guard ) # Embed manifest file set_target_properties(${EXECUTABLE_NAME} PROPERTIES LINK_FLAGS "/MANIFEST:EMBED /MANIFESTINPUT:${CMAKE_CURRENT_BINARY_DIR}/server/lemonade.manifest" ) endif() endif() # ============================================================ # Server core dependencies (PUBLIC โ€” inherited by consumers) # ============================================================ target_link_libraries(lemonade-server-core PUBLIC nlohmann_json::nlohmann_json) if(USE_SYSTEM_HTTPLIB) target_link_libraries(lemonade-server-core PUBLIC cpp-httplib) else() target_link_libraries(lemonade-server-core PUBLIC httplib::httplib) endif() if(USE_SYSTEM_CLI11) target_include_directories(lemonade-server-core PUBLIC ${CLI11_INCLUDE_DIRS}) else() target_link_libraries(lemonade-server-core PUBLIC CLI11::CLI11) endif() if(USE_SYSTEM_CURL) target_link_libraries(lemonade-server-core PUBLIC ${CURL_LIBRARIES}) target_include_directories(lemonade-server-core PUBLIC ${CURL_INCLUDE_DIRS}) target_compile_options(lemonade-server-core PUBLIC ${CURL_CFLAGS_OTHER}) else() target_link_libraries(lemonade-server-core PUBLIC libcurl) endif() if(USE_SYSTEM_ZSTD) target_link_libraries(lemonade-server-core PUBLIC ${ZSTD_LIBRARIES}) target_include_directories(lemonade-server-core PUBLIC ${ZSTD_INCLUDE_DIRS}) target_link_directories(lemonade-server-core PUBLIC ${ZSTD_LIBRARY_DIRS}) target_compile_options(lemonade-server-core PUBLIC ${ZSTD_CFLAGS_OTHER}) endif() if(USE_SYSTEM_HTTPLIB AND HTTPLIB_INCLUDE_DIRS) target_include_directories(lemonade-server-core PUBLIC ${HTTPLIB_INCLUDE_DIRS}) endif() # libwebsockets for the realtime and log-stream WebSocket server if(USE_SYSTEM_LWS) target_link_libraries(lemonade-server-core PUBLIC ${LWS_LIBRARIES}) target_include_directories(lemonade-server-core PUBLIC ${LWS_INCLUDE_DIRS}) target_link_directories(lemonade-server-core PUBLIC ${LWS_LIBRARY_DIRS}) target_compile_options(lemonade-server-core PUBLIC ${LWS_CFLAGS_OTHER}) else() target_link_libraries(lemonade-server-core PUBLIC websockets) target_include_directories(lemonade-server-core PUBLIC ${libwebsockets_BINARY_DIR}/include ${libwebsockets_SOURCE_DIR}/include) endif() # Enable ARC (Automatic Reference Counting) for macOS Objective-C++ files if(APPLE) target_compile_options(lemonade-server-core PUBLIC -fobjc-arc) endif() # Platform-specific linking if(WIN32) target_link_libraries(lemonade-server-core PUBLIC ws2_32 wsock32 wbemuuid ole32 oleaut32 ) elseif(APPLE) target_link_libraries(lemonade-server-core PUBLIC "-framework Metal" "-framework Foundation" "-framework CoreServices" "-lobjc" ) target_link_options(lemonade-server-core PUBLIC -Wl,-no_weak_imports) elseif(UNIX) # Link systemd for journal support if(PkgConfig_FOUND) pkg_check_modules(SYSTEMD QUIET libsystemd) if(SYSTEMD_FOUND) set(LEMONADE_SYSTEMD_UNIT_NAME "lemonade-server.service" CACHE STRING "Systemd unit name to match for journald log mode (overridable at runtime via LEMONADE_SYSTEMD_UNIT env var)") target_include_directories(lemonade-server-core PUBLIC ${SYSTEMD_INCLUDE_DIRS}) target_link_libraries(lemonade-server-core PUBLIC ${SYSTEMD_LIBRARIES}) target_compile_definitions(lemonade-server-core PUBLIC HAVE_SYSTEMD LEMONADE_SYSTEMD_UNIT_NAME="${LEMONADE_SYSTEMD_UNIT_NAME}") endif() endif() # Link libcap for capability management on Linux if(CMAKE_SYSTEM_NAME STREQUAL "Linux") if(PkgConfig_FOUND) pkg_check_modules(LIBCAP QUIET libcap) if(LIBCAP_FOUND) target_include_directories(lemonade-server-core PUBLIC ${LIBCAP_INCLUDE_DIRS}) target_link_libraries(lemonade-server-core PUBLIC ${LIBCAP_LIBRARIES}) target_compile_definitions(lemonade-server-core PUBLIC HAVE_LIBCAP) message(STATUS "libcap found - capability inheritance will be enabled") else() message(STATUS "libcap not found - capability inheritance will be disabled") endif() endif() endif() endif() # lemond inherits all deps from lemonade-server-core target_link_libraries(${EXECUTABLE_NAME} PRIVATE lemonade-server-core) # ============================================================ # Resources # ============================================================ # Resources are self-contained in src/cpp/resources/: # - static/index.html, static/favicon.ico (web UI) # - server_models.json (model registry) # - backend_versions.json (backend version configuration) # - defaults.json (default config values) # Collect resource files for dependency tracking file(GLOB_RECURSE RESOURCE_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/resources/*" ) # Create a custom target that depends on resource files # This ensures resources are copied when source files change add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/resources.stamp COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/resources ${CMAKE_BINARY_DIR}/resources COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_CURRENT_BINARY_DIR}/resources.stamp DEPENDS ${RESOURCE_FILES} COMMENT "Copying resources to build directory" ) add_custom_target(copy_resources ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/resources.stamp ) # Copy resources to the runtime directory after build add_custom_command(TARGET ${EXECUTABLE_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_BINARY_DIR}/resources $/resources COMMENT "Copying resources to output directory" ) # Ensure resources are copied before building the executable add_dependencies(${EXECUTABLE_NAME} copy_resources) # ============================================================ # Electron App Integration (Cross-Platform) # ============================================================ # Find Node.js and npm for building the Electron app find_program(NODE_EXECUTABLE node) # On Windows, npm is a batch file (npm.cmd), so we need to find it explicitly # or invoke it through cmd.exe to avoid MSBuild custom build step issues if(WIN32) find_program(NPM_EXECUTABLE npm.cmd) else() find_program(NPM_EXECUTABLE npm) endif() # Custom target to build the Electron app (must be called manually) if(NODE_EXECUTABLE AND NPM_EXECUTABLE) if(WIN32) set(ELECTRON_BUILD_TARGET "build:win") # On Windows, wrap npm calls with cmd /c to handle batch file execution properly set(NPM_COMMAND cmd /c "${NPM_EXECUTABLE}") elseif(APPLE) set(ELECTRON_BUILD_TARGET "build:mac") set(NPM_COMMAND "${NPM_EXECUTABLE}") else() set(ELECTRON_BUILD_TARGET "build:linux") set(NPM_COMMAND "${NPM_EXECUTABLE}") endif() set(ELECTRON_BUILD_SOURCE_DIR "${CMAKE_BINARY_DIR}/app-src") set(ELECTRON_APP_OUTPUT "${ELECTRON_APP_UNPACKED_DIR}/${ELECTRON_EXE_NAME}") file(GLOB_RECURSE ELECTRON_APP_SOURCES CONFIGURE_DEPENDS "${ELECTRON_APP_SOURCE_DIR}/src/*" "${ELECTRON_APP_SOURCE_DIR}/assets/*" "${ELECTRON_APP_SOURCE_DIR}/*.json" "${ELECTRON_APP_SOURCE_DIR}/*.js" "${ELECTRON_APP_SOURCE_DIR}/*.ts" "${ELECTRON_APP_SOURCE_DIR}/*.tsx" "${ELECTRON_APP_SOURCE_DIR}/*.css" "${ELECTRON_APP_SOURCE_DIR}/*.html" ) list(FILTER ELECTRON_APP_SOURCES EXCLUDE REGEX "[/\\\\](node_modules|dist|dist-app)[/\\\\]") # Note: On Windows, NPM_COMMAND uses 'cmd /c' wrapper to handle npm.cmd batch file # Note: Avoid shell-special characters in echo messages (no parentheses, etc.) add_custom_command( OUTPUT "${ELECTRON_APP_OUTPUT}" COMMAND ${CMAKE_COMMAND} -E echo "Copying Electron app source to build directory..." COMMAND ${CMAKE_COMMAND} -E remove_directory "${ELECTRON_BUILD_SOURCE_DIR}" COMMAND ${CMAKE_COMMAND} -E make_directory "${ELECTRON_BUILD_SOURCE_DIR}" COMMAND ${CMAKE_COMMAND} -E copy_directory "${ELECTRON_APP_SOURCE_DIR}" "${ELECTRON_BUILD_SOURCE_DIR}" COMMAND ${CMAKE_COMMAND} -E echo "Installing npm dependencies..." COMMAND ${CMAKE_COMMAND} -E chdir "${ELECTRON_BUILD_SOURCE_DIR}" ${NPM_COMMAND} ci COMMAND ${CMAKE_COMMAND} -E echo "Building Electron app for current platform..." COMMAND ${CMAKE_COMMAND} -E chdir "${ELECTRON_BUILD_SOURCE_DIR}" ${NPM_COMMAND} run ${ELECTRON_BUILD_TARGET} -- --config.directories.output=${ELECTRON_APP_BUILD_DIR} DEPENDS ${ELECTRON_APP_SOURCES} COMMENT "Building Electron app with npm" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" VERBATIM ) add_custom_target(electron-app DEPENDS "${ELECTRON_APP_OUTPUT}" ) message(STATUS "Node.js found: ${NODE_EXECUTABLE}") message(STATUS "npm found: ${NPM_EXECUTABLE}") if(WIN32) message(STATUS "Electron app can be built with: cmake --build --preset windows --target electron-app") else() message(STATUS "Electron app can be built with: cmake --build --preset default --target electron-app") endif() else() message(STATUS "Node.js or npm not found - Electron app build target disabled") message(STATUS "Install Node.js to enable: https://nodejs.org/") endif() # ============================================================ # Web App Integration (Cross-Platform) # ============================================================ # On Windows, disable automatic build by default (can be enabled with -DBUILD_WEB_APP=ON) # On other platforms, enable automatic build by default (can be disabled with -DBUILD_WEB_APP=OFF) if(WIN32) option(BUILD_WEB_APP "Build the Web app automatically" OFF) else() option(BUILD_WEB_APP "Build the Web app automatically" ON) endif() # Check if webpack is available for system-package mode if(USE_SYSTEM_NODEJS_MODULES) find_program(WEBPACK_EXECUTABLE webpack) if(WEBPACK_EXECUTABLE) message(STATUS "Using system webpack: ${WEBPACK_EXECUTABLE}") set(CAN_BUILD_WEB_APP TRUE) endif() else() # Non-system mode requires npm for dependency install + build if(NODE_EXECUTABLE AND NPM_EXECUTABLE) set(CAN_BUILD_WEB_APP TRUE) endif() endif() # Custom target to build the Web app with proper dependency tracking if(CAN_BUILD_WEB_APP AND BUILD_WEB_APP) # Create a stamp file to track build state set(WEB_APP_STAMP "${CMAKE_CURRENT_BINARY_DIR}/web-app.stamp") set(WEB_APP_BUILD_SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/web-app-src") # Get all source files in src/web-app (excluding node_modules and dist) file(GLOB_RECURSE WEB_APP_SOURCES CONFIGURE_DEPENDS "${WEB_APP_SOURCE_DIR}/src/*" "${WEB_APP_SOURCE_DIR}/*.json" "${WEB_APP_SOURCE_DIR}/*.js" "${WEB_APP_SOURCE_DIR}/*.ts" "${WEB_APP_SOURCE_DIR}/*.tsx" "${WEB_APP_SOURCE_DIR}/*.css" ) # Add custom command to build web app (only runs if sources change) add_custom_command( OUTPUT ${WEB_APP_STAMP} COMMAND ${CMAKE_COMMAND} -DWEB_APP_SOURCE_DIR=${WEB_APP_SOURCE_DIR} -DWEB_APP_BUILD_SOURCE_DIR=${WEB_APP_BUILD_SOURCE_DIR} -DWEB_APP_BUILD_DIR=${WEB_APP_BUILD_DIR} -DNPM_EXECUTABLE=${NPM_EXECUTABLE} -DWEBPACK_EXECUTABLE=${WEBPACK_EXECUTABLE} -DUSE_SYSTEM_NODEJS_MODULES=${USE_SYSTEM_NODEJS_MODULES} -DWEB_APP_STAMP=${WEB_APP_STAMP} -P ${CMAKE_CURRENT_SOURCE_DIR}/src/web-app/BuildWebApp.cmake DEPENDS ${WEB_APP_SOURCES} COMMENT "Building Web app" ) # Add custom target that depends on the stamp file add_custom_target(web-app ALL DEPENDS ${WEB_APP_STAMP}) message(STATUS "Web app will be built automatically (disable with -DBUILD_WEB_APP=OFF)") if (NOT WIN32 AND NOT APPLE AND NOT BUILD_ELECTRON_APP) # Install .desktop file for web app install(FILES ${CMAKE_SOURCE_DIR}/data/lemonade-web-app.desktop DESTINATION share/applications ) # Create symlink in standard applications path only if not installing to /usr if(NOT CMAKE_INSTALL_PREFIX STREQUAL "/usr") install(CODE " file(MAKE_DIRECTORY \"\$ENV{DESTDIR}/usr/share/applications\") execute_process( COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_INSTALL_PREFIX}/share/applications/lemonade-web-app.desktop \"\$ENV{DESTDIR}/usr/share/applications/lemonade-web-app.desktop\" ) ") endif() install(FILES ${CMAKE_SOURCE_DIR}/src/web-app/assets/logo.svg DESTINATION share/pixmaps RENAME lemonade-app.svg ) # Create symlink in standard pixmaps path only if not installing to /usr if(NOT CMAKE_INSTALL_PREFIX STREQUAL "/usr") install(CODE " file(MAKE_DIRECTORY \"\$ENV{DESTDIR}/usr/share/pixmaps\") execute_process( COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_INSTALL_PREFIX}/share/pixmaps/lemonade-app.svg \"\$ENV{DESTDIR}/usr/share/pixmaps/lemonade-app.svg\" ) ") endif() # Install launch script for web app install(FILES ${CMAKE_SOURCE_DIR}/data/lemonade-web-app DESTINATION bin PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) # Create symlink in standard bin path only if not installing to /usr if(NOT CMAKE_INSTALL_PREFIX STREQUAL "/usr") install(CODE " file(MAKE_DIRECTORY \"\$ENV{DESTDIR}/usr/bin\") execute_process( COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_INSTALL_PREFIX}/bin/lemonade-web-app \"\$ENV{DESTDIR}/usr/bin/lemonade-web-app\" ) ") endif() endif() else() if(NOT CAN_BUILD_WEB_APP) if(USE_SYSTEM_NODEJS_MODULES) message(STATUS "Web app build disabled - webpack not found (install webpack package)") else() message(STATUS "Web app build disabled - Node.js or npm not found") endif() else() message(STATUS "Web app automatic build disabled (enable with -DBUILD_WEB_APP=ON)") endif() endif() # Add manual web-app target for platforms where automatic build is disabled. # Reuse the shared build script so WiX/MSBuild gets output under ${WEB_APP_BUILD_DIR}. if(CAN_BUILD_WEB_APP AND NOT BUILD_WEB_APP) set(WEB_APP_BUILD_SOURCE_DIR_MANUAL "${CMAKE_CURRENT_BINARY_DIR}/web-app-manual-src") set(WEB_APP_MANUAL_STAMP "${CMAKE_CURRENT_BINARY_DIR}/web-app-manual.stamp") add_custom_target(web-app COMMAND ${CMAKE_COMMAND} -DWEB_APP_SOURCE_DIR=${WEB_APP_SOURCE_DIR} -DWEB_APP_BUILD_SOURCE_DIR=${WEB_APP_BUILD_SOURCE_DIR_MANUAL} -DWEB_APP_BUILD_DIR=${WEB_APP_BUILD_DIR} -DNPM_EXECUTABLE=${NPM_EXECUTABLE} -DWEBPACK_EXECUTABLE=${WEBPACK_EXECUTABLE} -DUSE_SYSTEM_NODEJS_MODULES=${USE_SYSTEM_NODEJS_MODULES} -DWEB_APP_STAMP=${WEB_APP_MANUAL_STAMP} -P ${CMAKE_CURRENT_SOURCE_DIR}/src/web-app/BuildWebApp.cmake COMMENT "Building Web app" WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" VERBATIM ) message(STATUS "Web app can be built manually with: cmake --build . --target web-app") endif() # ============================================================ # macOS Packaging & Notarization # ============================================================ if(APPLE) # SETUP IDENTITIES set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") set(CPACK_PACKAGE_VENDOR "Lemonade") set(CPACK_PACKAGE_CONTACT "lemonade@amd.com") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Lemonade Local LLM Server") set(CPACK_PACKAGE_DESCRIPTION "A lightweight, high-performance local LLM server.") set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/../../LICENSE") # Skip readme page entirely set(CPACK_INSTALL_README_DIR "") if(NOT DEFINED SIGNING_IDENTITY) if(DEFINED ENV{DEVELOPER_ID_APPLICATION_IDENTITY}) set(SIGNING_IDENTITY "$ENV{DEVELOPER_ID_APPLICATION_IDENTITY}") else() set(SIGNING_IDENTITY "BBC1E1924FF481293CF0928B5DFFF8A6419D7DA0") endif() endif() if(NOT DEFINED INSTALLER_IDENTITY) if(DEFINED ENV{DEVELOPER_ID_INSTALLER_IDENTITY}) set(INSTALLER_IDENTITY "$ENV{DEVELOPER_ID_INSTALLER_IDENTITY}") else() set(INSTALLER_IDENTITY "B69167355F20D364EE8FD7126D767B93B5344EBB") endif() endif() # Configure Plists & Entitlements configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/com.lemonade.server.plist.in ${CMAKE_CURRENT_BINARY_DIR}/com.lemonade.server.plist @ONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/com.lemonade.tray.plist.in ${CMAKE_CURRENT_BINARY_DIR}/com.lemonade.tray.plist @ONLY) set(ENTITLEMENTS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/server/entitlements.plist") if(NOT EXISTS "${ENTITLEMENTS_PATH}") file(WRITE "${ENTITLEMENTS_PATH}" " com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.disable-library-validation com.apple.security.cs.allow-dyld-environment-variables ") endif() endif() # Add tray application subdirectory add_subdirectory(src/cpp/tray) # Add CLI application subdirectory add_subdirectory(src/cpp/cli) # Add legacy shim (backwards-compatible lemonade-server) add_subdirectory(src/cpp/legacy-cli) # Wire WiX installer targets to the binaries and frontend artifacts they package. # Doing this after the subdirectories are added ensures MSBuild gets real project refs. if(WIN32 AND WIX_EXECUTABLE) set(WIX_PACKAGED_TARGETS) if(TARGET LemonadeServer) list(APPEND WIX_PACKAGED_TARGETS LemonadeServer) endif() if(TARGET lemonade) list(APPEND WIX_PACKAGED_TARGETS lemonade) endif() if(TARGET lemonade-server) list(APPEND WIX_PACKAGED_TARGETS lemonade-server) endif() if(TARGET ${EXECUTABLE_NAME}) list(APPEND WIX_PACKAGED_TARGETS ${EXECUTABLE_NAME}) endif() if(TARGET wix_installer_minimal) if(TARGET web-app) add_dependencies(wix_installer_minimal web-app) endif() if(WIX_PACKAGED_TARGETS) add_dependencies(wix_installer_minimal ${WIX_PACKAGED_TARGETS}) endif() endif() if(TARGET wix_installer_full) if(TARGET electron-app) add_dependencies(wix_installer_full electron-app) endif() if(TARGET web-app) add_dependencies(wix_installer_full web-app) endif() if(WIX_PACKAGED_TARGETS) add_dependencies(wix_installer_full ${WIX_PACKAGED_TARGETS}) endif() endif() endif() if(APPLE) # SIGNING LOGIC (Release Only) if(CMAKE_BUILD_TYPE STREQUAL "Release") message(STATUS "Release Build: Configuring Hardened Runtime Signing for Router") # Sign 'lemond' add_custom_command(TARGET ${EXECUTABLE_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E echo "--- Signing ${EXECUTABLE_NAME} ---" COMMAND codesign --force --options runtime --timestamp --entitlements "${ENTITLEMENTS_PATH}" --sign "${SIGNING_IDENTITY}" -v $ COMMENT "Signing ${EXECUTABLE_NAME} with Hardened Runtime" VERBATIM ) # Note: 'lemonade-server' is signed at install time below (line ~1110) endif() # Prepare Electron App set(ELECTRON_BUILD_COPY "${CMAKE_BINARY_DIR}/lemonade-app.app") add_custom_target(prepare_electron_app COMMAND ${CMAKE_COMMAND} -E remove_directory "${ELECTRON_BUILD_COPY}" COMMAND cp -R "${ELECTRON_APP_BUILD_DIR}/mac-arm64/lemonade-app.app" "${CMAKE_BINARY_DIR}/" || cp -R "${ELECTRON_APP_BUILD_DIR}/mac/lemonade-app.app" "${CMAKE_BINARY_DIR}/" || cp -R "${ELECTRON_APP_BUILD_DIR}/lemonade-app.app" "${CMAKE_BINARY_DIR}/" || echo "Warning: Could not find lemonade-app.app in ${ELECTRON_APP_BUILD_DIR}" COMMENT "Copying Electron App..." DEPENDS electron-app ) set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_BINARY_DIR}/cpack_prebuild.cmake") file(WRITE "${CMAKE_BINARY_DIR}/cpack_prebuild.cmake" "message(STATUS \"Pre-build: Ensuring Electron app is prepared...\")\n" "execute_process(COMMAND ${CMAKE_COMMAND} --build \"${CMAKE_BINARY_DIR}\" --target prepare_electron_app)\n" ) # INSTALLATION (Component Structure) # Main Router -> /usr/local/bin install(TARGETS ${EXECUTABLE_NAME} RUNTIME DESTINATION "bin" COMPONENT Runtime) # Sign the router binary after installation (Release builds only) if(CMAKE_BUILD_TYPE STREQUAL "Release") install(CODE " execute_process( COMMAND codesign --force --options runtime --timestamp --entitlements \"${ENTITLEMENTS_PATH}\" --sign \"${SIGNING_IDENTITY}\" -v \"${CMAKE_BINARY_DIR}/_CPack_Packages/Darwin/productbuild/Lemonade-${PROJECT_VERSION}-Darwin/Runtime/usr/local/bin/${EXECUTABLE_NAME}\" RESULT_VARIABLE SIGN_RESULT ) if(NOT SIGN_RESULT EQUAL 0) message(FATAL_ERROR \"Failed to sign ${EXECUTABLE_NAME}\") endif() " COMPONENT Runtime) endif() # Tray client -> /usr/local/bin if(TARGET lemonade-tray) install(TARGETS lemonade-tray RUNTIME DESTINATION "bin" COMPONENT Runtime) # Sign the tray binary after installation (Release builds only) if(CMAKE_BUILD_TYPE STREQUAL "Release") install(CODE " execute_process( COMMAND codesign --force --options runtime --timestamp --entitlements \"${ENTITLEMENTS_PATH}\" --sign \"${SIGNING_IDENTITY}\" -v \"${CMAKE_BINARY_DIR}/_CPack_Packages/Darwin/productbuild/Lemonade-${PROJECT_VERSION}-Darwin/Runtime/usr/local/bin/lemonade-tray\" RESULT_VARIABLE SIGN_RESULT ) if(NOT SIGN_RESULT EQUAL 0) message(FATAL_ERROR \"Failed to sign lemonade-tray\") endif() " COMPONENT Runtime) endif() endif() # Legacy shim -> /usr/local/bin if(TARGET lemonade-server) install(TARGETS lemonade-server RUNTIME DESTINATION "bin" COMPONENT Runtime) # Sign the legacy shim after installation (Release builds only) if(CMAKE_BUILD_TYPE STREQUAL "Release") install(CODE " execute_process( COMMAND codesign --force --options runtime --timestamp --entitlements \"${ENTITLEMENTS_PATH}\" --sign \"${SIGNING_IDENTITY}\" -v \"${CMAKE_BINARY_DIR}/_CPack_Packages/Darwin/productbuild/Lemonade-${PROJECT_VERSION}-Darwin/Runtime/usr/local/bin/lemonade-server\" RESULT_VARIABLE SIGN_RESULT ) if(NOT SIGN_RESULT EQUAL 0) message(FATAL_ERROR \"Failed to sign lemonade-server\") endif() " COMPONENT Runtime) endif() endif() # CLI -> /usr/local/bin if(TARGET lemonade) install(TARGETS lemonade RUNTIME DESTINATION "bin" COMPONENT Runtime) # Sign the CLI binary after installation (Release builds only) if(CMAKE_BUILD_TYPE STREQUAL "Release") install(CODE " execute_process( COMMAND codesign --force --options runtime --timestamp --entitlements \"${ENTITLEMENTS_PATH}\" --sign \"${SIGNING_IDENTITY}\" -v \"${CMAKE_BINARY_DIR}/_CPack_Packages/Darwin/productbuild/Lemonade-${PROJECT_VERSION}-Darwin/Runtime/usr/local/bin/lemonade\" RESULT_VARIABLE SIGN_RESULT ) if(NOT SIGN_RESULT EQUAL 0) message(FATAL_ERROR \"Failed to sign lemonade\") endif() " COMPONENT Runtime) endif() endif() # Resources -> /Library/Application Support/Lemonade/resources install(DIRECTORY ${CMAKE_BINARY_DIR}/resources/ DESTINATION "/Library/Application Support/Lemonade/resources" COMPONENT Resources) # App Bundle -> /Applications install(DIRECTORY "${ELECTRON_BUILD_COPY}" DESTINATION "/Applications" USE_SOURCE_PERMISSIONS COMPONENT Applications ) # Plists -> /Library/... install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.lemonade.server.plist" DESTINATION "/Library/LaunchDaemons" COMPONENT Runtime) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/com.lemonade.tray.plist" DESTINATION "/Library/LaunchAgents" COMPONENT Runtime) # Placeholder -> /usr/local/share/lemonade-server file(WRITE "${CMAKE_BINARY_DIR}/.keep" "") install(FILES "${CMAKE_BINARY_DIR}/.keep" DESTINATION "usr/local/share/lemonade-server" COMPONENT Runtime) # ackaging & Notarization set(CPACK_GENERATOR "productbuild") set(CPACK_PACKAGE_NAME "Lemonade") set(CPACK_PRODUCTBUILD_IDENTIFIER "com.lemonade.server") set(CPACK_PRODUCTBUILD_DOMAIN "system") if(INSTALLER_IDENTITY) set(CPACK_PRODUCTBUILD_IDENTITY_NAME "${INSTALLER_IDENTITY}") endif() set(CPACK_PRODUCTBUILD_RELOCATE OFF) # Prevent install_name_tool from invalidating signatures set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE) set(CPACK_PRODUCTBUILD_BUNDLE com.lemonade.server.Applications OFF) set(CPACK_PRODUCTBUILD_BUNDLE_ID com.lemonade.server.Applications) set(CPACK_PACKAGING_INSTALL_PREFIX "/usr/local") set(CPACK_SET_DESTDIR ON) set(CPACK_PACKAGE_RELOCATABLE OFF) set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF) set(CPACK_COMPONENT_APPLICATIONS_IS_ABSOLUTE ON) # license file extension for CPack configure_file("${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" "${CMAKE_BINARY_DIR}/LICENSE.txt" COPYONLY) set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_BINARY_DIR}/LICENSE.txt") set(CPACK_POSTFLIGHT_RUNTIME_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/postinst-full-mac") include(CPack) set(PACKAGE_DEPENDS prepare_electron_app ${EXECUTABLE_NAME}) add_custom_target(package-macos DEPENDS ${PACKAGE_DEPENDS} COMMAND ${CMAKE_COMMAND} --build . --config $ --target package COMMENT "Running CPack to create signed installer..." VERBATIM ) find_program(XCRUN_EXE xcrun) if(XCRUN_EXE) set(PKG_NAME "Lemonade-${PROJECT_VERSION}-Darwin.pkg") add_custom_target(notarize COMMAND ${CMAKE_COMMAND} -E echo "Submitting ${PKG_NAME} for notarization..." COMMAND ${XCRUN_EXE} notarytool submit "${CMAKE_BINARY_DIR}/${PKG_NAME}" --keychain-profile "AC_PASSWORD" --wait --verbose COMMAND ${CMAKE_COMMAND} -E echo "Stapling ticket to ${PKG_NAME}..." COMMAND ${XCRUN_EXE} stapler staple "${CMAKE_BINARY_DIR}/${PKG_NAME}" WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMENT "Notarizing and Stapling macOS package..." VERBATIM ) add_dependencies(notarize package-macos) endif() endif() # ============================================================ # CPack Configuration (Linux only) # ============================================================ if(UNIX AND NOT APPLE) set(CPACK_SET_DESTDIR ON) set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") set(CPACK_PACKAGE_VENDOR "Lemonade") set(CPACK_PACKAGE_CONTACT "lemonade@amd.com") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Lemonade Local LLM Server") set(CPACK_PACKAGE_DESCRIPTION "A lightweight, high-performance local LLM server with support for multiple backends including llama.cpp, FastFlowLM, and RyzenAI.") # Only install our executables (not curl/development files) # Note: lemonade-server installation is handled in tray/CMakeLists.txt install(TARGETS ${EXECUTABLE_NAME} RUNTIME DESTINATION bin ) # Install resources (all files including KaTeX fonts) install(DIRECTORY ${CMAKE_BINARY_DIR}/resources/ DESTINATION share/lemonade-server/resources ) # Install packaged config defaults for distro-managed overrides. install(FILES ${CMAKE_BINARY_DIR}/resources/defaults.json DESTINATION share/lemonade ) # Install example scripts install(DIRECTORY ${CMAKE_SOURCE_DIR}/examples/ DESTINATION share/lemonade-server/examples USE_SOURCE_PERMISSIONS PATTERN "*.sh" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) # Configure systemd service file (uses CMAKE_INSTALL_FULL_* variables) configure_file( ${CMAKE_SOURCE_DIR}/data/lemonade-server.service.in ${CMAKE_BINARY_DIR}/lemonade-server.service @ONLY ) # Install systemd service file install(FILES ${CMAKE_BINARY_DIR}/lemonade-server.service DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/systemd/system ) # Create symlink in standard systemd search path only if not installing to /usr # (if prefix is /usr, the service is already in the standard location) if(NOT CMAKE_INSTALL_PREFIX STREQUAL "/usr") install(CODE " file(MAKE_DIRECTORY \"\$ENV{DESTDIR}/usr/lib/systemd/system\") execute_process( COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_INSTALL_PREFIX}/lib/systemd/system/lemonade-server.service \"\$ENV{DESTDIR}/usr/lib/systemd/system/lemonade-server.service\" ) ") endif() # Install secrets.conf for LEMONADE_API_KEY (still loaded via EnvironmentFile) install(FILES ${CMAKE_SOURCE_DIR}/data/secrets.conf DESTINATION /etc/lemonade/conf.d RENAME zz-secrets.conf PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ GROUP_WRITE ) # Create symlinks for KaTeX fonts if they exist on the system # Check if fonts-katex package fonts are available set(KATEX_FONTS_DIR "/usr/share/fonts/truetype/katex") if(EXISTS "${KATEX_FONTS_DIR}") message(STATUS "KaTeX fonts found at ${KATEX_FONTS_DIR}, creating symlinks") # Ensure web-app directory exists in build directory file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/resources/web-app") set(KATEX_FONTS "KaTeX_AMS-Regular.ttf" "KaTeX_AMS-Regular.woff" "KaTeX_AMS-Regular.woff2" "KaTeX_Caligraphic-Bold.ttf" "KaTeX_Caligraphic-Bold.woff" "KaTeX_Caligraphic-Bold.woff2" "KaTeX_Caligraphic-Regular.ttf" "KaTeX_Caligraphic-Regular.woff" "KaTeX_Caligraphic-Regular.woff2" "KaTeX_Fraktur-Bold.ttf" "KaTeX_Fraktur-Bold.woff" "KaTeX_Fraktur-Bold.woff2" "KaTeX_Fraktur-Regular.ttf" "KaTeX_Fraktur-Regular.woff" "KaTeX_Fraktur-Regular.woff2" "KaTeX_Main-Bold.ttf" "KaTeX_Main-Bold.woff" "KaTeX_Main-Bold.woff2" "KaTeX_Main-BoldItalic.ttf" "KaTeX_Main-BoldItalic.woff" "KaTeX_Main-BoldItalic.woff2" "KaTeX_Main-Italic.ttf" "KaTeX_Main-Italic.woff" "KaTeX_Main-Italic.woff2" "KaTeX_Main-Regular.ttf" "KaTeX_Main-Regular.woff" "KaTeX_Main-Regular.woff2" "KaTeX_Math-BoldItalic.ttf" "KaTeX_Math-BoldItalic.woff" "KaTeX_Math-BoldItalic.woff2" "KaTeX_Math-Italic.ttf" "KaTeX_Math-Italic.woff" "KaTeX_Math-Italic.woff2" "KaTeX_SansSerif-Bold.ttf" "KaTeX_SansSerif-Bold.woff" "KaTeX_SansSerif-Bold.woff2" "KaTeX_SansSerif-Italic.ttf" "KaTeX_SansSerif-Italic.woff" "KaTeX_SansSerif-Italic.woff2" "KaTeX_SansSerif-Regular.ttf" "KaTeX_SansSerif-Regular.woff" "KaTeX_SansSerif-Regular.woff2" "KaTeX_Script-Regular.ttf" "KaTeX_Script-Regular.woff" "KaTeX_Script-Regular.woff2" "KaTeX_Size1-Regular.ttf" "KaTeX_Size1-Regular.woff" "KaTeX_Size1-Regular.woff2" "KaTeX_Size2-Regular.ttf" "KaTeX_Size2-Regular.woff" "KaTeX_Size2-Regular.woff2" "KaTeX_Size3-Regular.ttf" "KaTeX_Size3-Regular.woff" "KaTeX_Size3-Regular.woff2" "KaTeX_Size4-Regular.ttf" "KaTeX_Size4-Regular.woff" "KaTeX_Size4-Regular.woff2" "KaTeX_Typewriter-Regular.ttf" "KaTeX_Typewriter-Regular.woff" "KaTeX_Typewriter-Regular.woff2" ) foreach(font ${KATEX_FONTS}) set(FONT_LINK "${CMAKE_BINARY_DIR}/resources/web-app/${font}") set(FONT_TARGET "${KATEX_FONTS_DIR}/${font}") # Only create symlink if the target font exists if(EXISTS "${FONT_TARGET}") # Remove existing file/symlink if present if(EXISTS "${FONT_LINK}" OR IS_SYMLINK "${FONT_LINK}") file(REMOVE "${FONT_LINK}") endif() # Create symlink in build directory file(CREATE_LINK "${FONT_TARGET}" "${FONT_LINK}" SYMBOLIC) endif() endforeach() else() message(STATUS "KaTeX fonts not found at ${KATEX_FONTS_DIR}, using bundled fonts") endif() # Check if Electron app is available for full package option(BUILD_ELECTRON_APP "Build and include Electron app in deb package" OFF) # Build electron app if requested and target is available if(BUILD_ELECTRON_APP AND TARGET electron-app) message(STATUS "BUILD_ELECTRON_APP is ON - building Electron app...") execute_process( COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target electron-app RESULT_VARIABLE ELECTRON_BUILD_RESULT ) if(NOT ELECTRON_BUILD_RESULT EQUAL 0) message(FATAL_ERROR "Failed to build Electron app") endif() endif() if(BUILD_ELECTRON_APP AND EXISTS "${ELECTRON_APP_UNPACKED_DIR}/${ELECTRON_EXE_NAME}") message(STATUS "Electron app found at ${ELECTRON_APP_UNPACKED_DIR}") message(STATUS "Building full deb package with Electron app") # Full package name set(CPACK_PACKAGE_NAME "lemonade-server") # Install Electron app install(DIRECTORY "${ELECTRON_APP_UNPACKED_DIR}/" DESTINATION share/lemonade-server/app USE_SOURCE_PERMISSIONS PATTERN "*.so" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) if (NOT WIN32 AND NOT APPLE) if (EXISTS "${ELECTRON_APP_UNPACKED_DIR}/chrome-sandbox") install(FILES "${ELECTRON_APP_UNPACKED_DIR}/chrome-sandbox" DESTINATION share/lemonade-server/app PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE SETUID ) endif() if (EXISTS "${ELECTRON_APP_UNPACKED_DIR}/chrome_crashpad_handler") install(FILES "${ELECTRON_APP_UNPACKED_DIR}/chrome_crashpad_handler" DESTINATION share/lemonade-server/app PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE ) endif() endif() if (NOT WIN32 AND NOT APPLE) # Install .desktop file for electron app install(FILES ${CMAKE_SOURCE_DIR}/data/lemonade-app.desktop DESTINATION share/applications ) # Create symlink in standard applications path only if not installing to /usr if(NOT CMAKE_INSTALL_PREFIX STREQUAL "/usr") install(CODE " file(MAKE_DIRECTORY \"\$ENV{DESTDIR}/usr/share/applications\") execute_process( COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_INSTALL_PREFIX}/share/applications/lemonade-app.desktop \"\$ENV{DESTDIR}/usr/share/applications/lemonade-app.desktop\" ) ") endif() install(FILES ${CMAKE_SOURCE_DIR}/src/app/assets/logo.svg DESTINATION share/pixmaps RENAME lemonade-app.svg ) # Create symlink in standard pixmaps path only if not installing to /usr if(NOT CMAKE_INSTALL_PREFIX STREQUAL "/usr") install(CODE " file(MAKE_DIRECTORY \"\$ENV{DESTDIR}/usr/share/pixmaps\") execute_process( COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_INSTALL_PREFIX}/share/pixmaps/lemonade-app.svg \"\$ENV{DESTDIR}/usr/share/pixmaps/lemonade-app.svg\" ) ") endif() # Create symlink in standard bin path only if not installing to /usr if(NOT CMAKE_INSTALL_PREFIX STREQUAL "/usr") install(CODE " file(MAKE_DIRECTORY \"\$ENV{DESTDIR}/usr/bin\") execute_process( COMMAND ${CMAKE_COMMAND} -E create_symlink \"\${CMAKE_INSTALL_PREFIX}/share/lemonade-server/app/lemonade\" \"\$ENV{DESTDIR}/usr/bin/lemonade-app\" ) ") else() install(CODE " file(CREATE_LINK \"\${CMAKE_INSTALL_PREFIX}/share/lemonade-server/app/lemonade\" \"\${CMAKE_INSTALL_PREFIX}/bin/lemonade-app\" SYMBOLIC) ") endif() endif() else() set(CPACK_PACKAGE_NAME "lemonade-server") endif() # RPM specific variables defined within include(${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/CPackRPM.cmake) include(CPack) # ============================================================ # AppImage Build Target (Linux only) # ============================================================ option(BUILD_APPIMAGE "Build the AppImage automatically" OFF) if(NODE_EXECUTABLE AND NPM_EXECUTABLE) # Set the build directory and source directory for AppImage set(APPIMAGE_BUILD_SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/app-appimage-src") set(APPIMAGE_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/app-appimage") if(BUILD_APPIMAGE) add_custom_target(appimage ALL COMMAND ${CMAKE_COMMAND} -E echo "Copying Electron app source to AppImage build directory..." COMMAND ${CMAKE_COMMAND} -E remove_directory "${APPIMAGE_BUILD_SOURCE_DIR}" COMMAND ${CMAKE_COMMAND} -E make_directory "${APPIMAGE_BUILD_SOURCE_DIR}" COMMAND ${CMAKE_COMMAND} -E copy_directory "${ELECTRON_APP_SOURCE_DIR}" "${APPIMAGE_BUILD_SOURCE_DIR}" COMMAND ${CMAKE_COMMAND} -E echo "Setting version to ${PROJECT_VERSION}..." COMMAND ${CMAKE_COMMAND} -E chdir "${APPIMAGE_BUILD_SOURCE_DIR}" ${NPM_COMMAND} version ${PROJECT_VERSION} --no-git-tag-version --allow-same-version COMMAND ${CMAKE_COMMAND} -E echo "Installing npm dependencies for AppImage build..." COMMAND ${CMAKE_COMMAND} -E chdir "${APPIMAGE_BUILD_SOURCE_DIR}" ${NPM_COMMAND} ci COMMAND ${CMAKE_COMMAND} -E echo "Building AppImage with electron-builder..." COMMAND ${CMAKE_COMMAND} -E chdir "${APPIMAGE_BUILD_SOURCE_DIR}" ${NPM_COMMAND} run build:renderer:prod COMMAND ${CMAKE_COMMAND} -E chdir "${APPIMAGE_BUILD_SOURCE_DIR}" ${NPM_EXECUTABLE} exec -- electron-builder --linux AppImage --config.directories.output=${APPIMAGE_BUILD_DIR} COMMAND ${CMAKE_COMMAND} -E echo "AppImage build complete. Output in ${APPIMAGE_BUILD_DIR}" COMMENT "Building Electron app as AppImage" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" ) message(STATUS "AppImage will be built automatically (disable with -DBUILD_APPIMAGE=OFF)") else() add_custom_target(appimage COMMAND ${CMAKE_COMMAND} -E echo "Copying Electron app source to AppImage build directory..." COMMAND ${CMAKE_COMMAND} -E remove_directory "${APPIMAGE_BUILD_SOURCE_DIR}" COMMAND ${CMAKE_COMMAND} -E make_directory "${APPIMAGE_BUILD_SOURCE_DIR}" COMMAND ${CMAKE_COMMAND} -E copy_directory "${ELECTRON_APP_SOURCE_DIR}" "${APPIMAGE_BUILD_SOURCE_DIR}" COMMAND ${CMAKE_COMMAND} -E echo "Setting version to ${PROJECT_VERSION}..." COMMAND ${CMAKE_COMMAND} -E chdir "${APPIMAGE_BUILD_SOURCE_DIR}" ${NPM_COMMAND} version ${PROJECT_VERSION} --no-git-tag-version --allow-same-version COMMAND ${CMAKE_COMMAND} -E echo "Installing npm dependencies for AppImage build..." COMMAND ${CMAKE_COMMAND} -E chdir "${APPIMAGE_BUILD_SOURCE_DIR}" ${NPM_COMMAND} ci COMMAND ${CMAKE_COMMAND} -E echo "Building AppImage with electron-builder..." COMMAND ${CMAKE_COMMAND} -E chdir "${APPIMAGE_BUILD_SOURCE_DIR}" ${NPM_COMMAND} run build:renderer:prod COMMAND ${CMAKE_COMMAND} -E chdir "${APPIMAGE_BUILD_SOURCE_DIR}" ${NPM_EXECUTABLE} exec -- electron-builder --linux AppImage --config.directories.output=${APPIMAGE_BUILD_DIR} COMMAND ${CMAKE_COMMAND} -E echo "AppImage build complete. Output in ${APPIMAGE_BUILD_DIR}" COMMENT "Building Electron app as AppImage" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" ) message(STATUS "AppImage automatic build disabled (enable with -DBUILD_APPIMAGE=ON)") message(STATUS "AppImage can be built manually with: cmake --build . --target appimage") endif() else() message(STATUS "Node.js or npm not found - AppImage build target disabled") endif() endif() # ============================================================ # Embeddable Archive Target (Cross-Platform) # ============================================================ # Builds lemond + lemonade, assembles a portable archive with # binaries, LICENSE, and resource files. Produces .tar.gz on # Linux or .zip on Windows. # # Usage: # cmake --preset default -DBUILD_WEB_APP=OFF # cmake --build --preset default --target embeddable if(WIN32) set(EMBEDDABLE_PLATFORM "windows-x64") else() set(EMBEDDABLE_PLATFORM "ubuntu-x64") endif() set(EMBEDDABLE_DIR_NAME "lemonade-embeddable-${PROJECT_VERSION}-${EMBEDDABLE_PLATFORM}") set(EMBEDDABLE_STAGING_DIR "${CMAKE_BINARY_DIR}/${EMBEDDABLE_DIR_NAME}") if(WIN32) set(EMBEDDABLE_ARCHIVE_NAME "${EMBEDDABLE_DIR_NAME}.zip") set(EMBEDDABLE_TAR_CMD ${CMAKE_COMMAND} -E tar cf "${CMAKE_BINARY_DIR}/${EMBEDDABLE_ARCHIVE_NAME}" --format=zip "${EMBEDDABLE_DIR_NAME}") else() set(EMBEDDABLE_ARCHIVE_NAME "${EMBEDDABLE_DIR_NAME}.tar.gz") set(EMBEDDABLE_TAR_CMD ${CMAKE_COMMAND} -E tar czf "${CMAKE_BINARY_DIR}/${EMBEDDABLE_ARCHIVE_NAME}" "${EMBEDDABLE_DIR_NAME}") endif() add_custom_target(embeddable # Clean and create staging directory COMMAND ${CMAKE_COMMAND} -E remove_directory "${EMBEDDABLE_STAGING_DIR}" COMMAND ${CMAKE_COMMAND} -E make_directory "${EMBEDDABLE_STAGING_DIR}/resources" # Copy binaries COMMAND ${CMAKE_COMMAND} -E copy "$" "${EMBEDDABLE_STAGING_DIR}/" COMMAND ${CMAKE_COMMAND} -E copy "$" "${EMBEDDABLE_STAGING_DIR}/" # Copy LICENSE COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" "${EMBEDDABLE_STAGING_DIR}/" # Copy resource files COMMAND ${CMAKE_COMMAND} -E copy "$/resources/server_models.json" "${EMBEDDABLE_STAGING_DIR}/resources/" COMMAND ${CMAKE_COMMAND} -E copy "$/resources/backend_versions.json" "${EMBEDDABLE_STAGING_DIR}/resources/" COMMAND ${CMAKE_COMMAND} -E copy "$/resources/defaults.json" "${EMBEDDABLE_STAGING_DIR}/resources/" # Create archive COMMAND ${CMAKE_COMMAND} -E chdir "${CMAKE_BINARY_DIR}" ${EMBEDDABLE_TAR_CMD} DEPENDS lemond lemonade COMMENT "Assembling embeddable archive: ${EMBEDDABLE_ARCHIVE_NAME}" VERBATIM ) message(STATUS "Embeddable archive target available: cmake --build . --target embeddable") message(STATUS " Archive: ${EMBEDDABLE_ARCHIVE_NAME}") lemonade-sdk-lemonade-dbde812/CMakePresets.json000066400000000000000000000036451516551144000215650ustar00rootroot00000000000000{ "version": 5, "vendor": { "lemonade": {} }, "configurePresets": [ { "name": "default", "displayName": "Default (Ninja)", "description": "Build with Ninja generator", "generator": "Ninja", "binaryDir": "${sourceDir}/build", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } }, { "name": "windows", "displayName": "Windows (Visual Studio 2022)", "description": "Build with Visual Studio 2022 generator on Windows", "generator": "Visual Studio 17 2022", "architecture": "x64", "binaryDir": "${sourceDir}/build" }, { "name": "vs18", "displayName": "Windows (Visual Studio 2026)", "description": "Build with Visual Studio 2026 generator on Windows", "generator": "Visual Studio 18 2026", "architecture": "x64", "binaryDir": "${sourceDir}/build" }, { "name": "debug", "displayName": "Debug (Ninja)", "description": "Build with Ninja generator in Debug mode", "generator": "Ninja", "binaryDir": "${sourceDir}/build-debug", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } }, { "name": "release", "displayName": "Release (Ninja)", "description": "Build with Ninja generator in Release mode", "generator": "Ninja", "binaryDir": "${sourceDir}/build-release", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }, "hidden": true } ], "buildPresets": [ { "name": "default", "configurePreset": "default" }, { "name": "windows", "configurePreset": "windows", "configuration": "Release" }, { "name": "vs18", "configurePreset": "vs18", "configuration": "Release" }, { "name": "debug", "configurePreset": "debug" }, { "name": "release", "configurePreset": "release" } ] } lemonade-sdk-lemonade-dbde812/Dockerfile000066400000000000000000000052161516551144000203320ustar00rootroot00000000000000# ============================================================== # # 1. Build stage โ€” compile lemonade C++ binaries # # ============================================================ FROM ubuntu:24.04 AS builder # Avoid interactive prompts during build ENV DEBIAN_FRONTEND=noninteractive # Install build dependencies RUN apt-get update && apt-get install -y \ build-essential \ cmake \ ninja-build \ libssl-dev \ pkg-config \ libdrm-dev \ git \ nodejs \ npm \ && rm -rf /var/lib/apt/lists/* # Copy source code COPY . /app WORKDIR /app # Build the project RUN rm -rf build && \ cmake --preset default && \ cmake --build --preset default web-app # Debug: Check build outputs RUN echo "=== Build directory contents ===" && \ ls -la build/ && \ echo "=== Checking for resources ===" && \ find build/ -name "*.json" -o -name "resources" -type d # # ============================================================ # # 2. Runtime stage โ€” small, clean image # # ============================================================ FROM ubuntu:24.04 # Install runtime dependencies only RUN apt-get update && apt-get install -y \ libcurl4 \ curl \ libssl3 \ zlib1g \ libdrm2 \ vulkan-tools \ libvulkan1 \ unzip \ libgomp1 \ libatomic1 \ software-properties-common \ jq \ && rm -rf /var/lib/apt/lists/* RUN add-apt-repository -y ppa:amd-team/xrt # Create application directory WORKDIR /opt/lemonade # Copy built executables and resources from builder COPY --from=builder /app/build/lemond ./lemond COPY --from=builder /app/build/lemonade-server ./lemonade-server COPY --from=builder /app/build/lemonade ./lemonade COPY --from=builder /app/build/resources ./resources # Download and install FLM using version from backend_versions.json RUN FLM_VERSION=$(jq -r '.flm.npu' ./resources/backend_versions.json) && \ FLM_VERSION_NUM=$(echo $FLM_VERSION | sed 's/^v//') && \ curl -L -o fastflowlm.deb "https://github.com/FastFlowLM/FastFlowLM/releases/download/${FLM_VERSION}/fastflowlm_${FLM_VERSION_NUM}_ubuntu24.04_amd64.deb" && \ apt install -y ./fastflowlm.deb && \ rm fastflowlm.deb # Make executables executable RUN chmod +x ./lemond ./lemonade-server # Create necessary directories RUN mkdir -p /opt/lemonade/llama/cpu \ /opt/lemonade/llama/vulkan \ /root/.cache/huggingface # Expose default port EXPOSE 13305 # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:13305/live || exit 1 # Default command: start server in headless mode CMD ["./lemonade-server", "serve", "--no-tray", "--host", "0.0.0.0"] lemonade-sdk-lemonade-dbde812/LICENSE000066400000000000000000000261351516551144000173500ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. lemonade-sdk-lemonade-dbde812/NOTICE.md000066400000000000000000000172701516551144000176460ustar00rootroot00000000000000PORTIONS LICENSED AS FOLLOWS ## Lucide Icons SVG icon paths copied from Lucide (https://lucide.dev) are used in `src/app/src/renderer/components/Icons.tsx`. The following icons are included: ChevronLeft, ChevronRight, BookOpen, ExternalLink, Boxes, Cpu, Store, Settings, and SlidersHorizontal. These icons are licensed under the ISC license from https://github.com/lucide-icons/lucide. > ISC License > > Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022. > > Permission to use, copy, modify, and/or distribute this software for any > purpose with or without fee is hereby granted, provided that the above > copyright notice and this permission notice appear in all copies. > > THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES > WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF > MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR > ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES > WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN > ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF > OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ## amdxdna_accel.h The file `src/cpp/include/lemon/amdxdna_accel.h` is licensed under GPL-2.0 WITH Linux-syscall-note. The Linux-syscall-note exception allows this GPL-licensed kernel UAPI header to be included in userspace applications without imposing GPL requirements on the application itself. This exception is standard for Linux kernel headers that define the userspace API for system calls and ioctl operations. ## llama.cpp Binaries for llama.cpp are downloaded under the MIT license from https://github.com/ggml-org/llama.cpp, as well as https://github.com/lemonade-sdk/llamacpp-rocm (which uses https://github.com/ggml-org/llama.cpp to build them.) Lemonade SDK used the [ONNX TurnkeyML](https://github.com/onnx/turnkeyml) project as a starting point under the [Apache 2.0 license](./LICENSE). > MIT License > > Copyright (c) 2023-2024 The ggml authors > > 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. ## Ollama Lemonade's Ollama-compatible API is based on the [Ollama API specification](https://github.com/ollama/ollama/blob/main/docs/api.md), licensed under the MIT license from https://github.com/ollama/ollama. > MIT License > > Copyright (c) Ollama > > 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. ## TurnkeyML Attribution TurnkeyML used code from other open source projects as a starting point (see [NOTICE.md](NOTICE.md)). Thank you Philip Colangelo, Derek Elkins, Jeremy Fowers, Dan Gard, Victoria Godsoe, Mark Heaps, Daniel Holanda, Brian Kurtz, Mariah Larwood, Philip Lassen, Andrew Ling, Adrian Macias, Gary Malik, Sarah Massengill, Ashwin Murthy, Hatice Ozen, Tim Sears, Sean Settle, Krishna Sivakumar, Aviv Weinstein, Xueli Xao, Bill Xing, and Lev Zlotnik for your contributions to that work. \> TurnkeyML used code from the [MLAgility](https://github.com/groq/mlagility) and [GroqFlow](https://github.com/groq/groqflow) projects as a starting point. Much of that code was refactored, improved, or replaced by the time TurnkeyML was published. \> TurnkeyML uses the [Microsoft lemon emoji](https://github.com/microsoft/fluentui-emoji) as an icon for the lemoande tool. >The MIT License > >Copyright 2023 Groq Inc. > >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. ## aixlog aixlog is a header-only C++ logging library, licensed under the MIT license from https://github.com/badaix/aixlog. It has been imported as is with no modifications in the lemonade tree under the MIT license. It's purpose is a logging system. > MIT License > > Copyright (C) 2017-2021 Johannes Pohl > > 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. lemonade-sdk-lemonade-dbde812/README.md000066400000000000000000000436711516551144000176260ustar00rootroot00000000000000## ๐Ÿ‹ Lemonade: Refreshingly fast local AI

Discord PRs Welcome Latest Release GitHub downloads GitHub issues License: Apache Star History Chart

Lemonade Banner

Download | Documentation | Discord

Lemonade is the local AI server that gives you the same capabilities as cloud APIs, except 100% free and private. Use the latest models for chat, coding, speech, and image generation on your own NPU and GPU. Lemonade comes in two flavors: * **Lemonade Server** installs a service you can connect to hundreds of great apps using standard OpenAI, Anthropic, and Ollama APIs. * **Embeddable Lemonade** is a portable binary you can package into your own application to give it multi-modal local AI that auto-optimizes for your userโ€™s PC. *This project is built by the community for every PC, with optimizations by AMD engineers to get the most from Ryzen AI, Radeon, and Strix Halo PCs.* ## Getting Started 1. **Install**: [Windows](https://lemonade-server.ai/install_options.html#windows) ยท [Linux](https://lemonade-server.ai/install_options.html#linux) ยท [macOS (beta)](https://lemonade-server.ai/install_options.html#macos) ยท [Docker](https://lemonade-server.ai/install_options.html#docker) ยท [Source](./docs/dev-getting-started.md) 2. **Get Models**: Browse and download with the [Model Manager](#model-library) 3. **Generate**: Try models with the built-in interfaces for chat, image gen, speech gen, and more 4. **Mobile**: Take your lemonade to go: [iOS](https://apps.apple.com/us/app/lemonade-mobile/id6757372210) ยท [Android](https://play.google.com/store/apps/details?id=com.lemonade.mobile.chat.ai&pli=1) ยท [Source](https://github.com/lemonade-sdk/lemonade-mobile) 5. **Connect**: Use Lemonade with your [favorite apps](https://lemonade-server.ai/marketplace):

Claude Code  Firefox Chatbot  AnythingLLM  Dify  GAIA  GitHub Copilot  Infinity Arcade  n8n  Open WebUI  OpenHands

Want your app featured here? Just submit a marketplace PR!

## Supported Platforms | Platform | Build | |----------|-------| | [![Arch Linux](https://img.shields.io/badge/Arch%20Linux-supported-1793D1?logo=arch-linux&logoColor=white)](https://lemonade-server.ai/install_options.html#arch) | [![Build on Arch](https://img.shields.io/github/actions/workflow/status/lemonade-sdk/lemonade/linux_distro_builds.yml?branch=main&label=Build%20on%20Arch)](https://github.com/lemonade-sdk/lemonade/actions/workflows/linux_distro_builds.yml) | | [![Debian Trixie+](https://img.shields.io/badge/Debian-Trixie%2B-A81D33?logo=debian&logoColor=white)](https://lemonade-server.ai/install_options.html#debian) | [![Build on Debian](https://img.shields.io/github/actions/workflow/status/lemonade-sdk/lemonade/linux_distro_builds.yml?branch=main&label=Build%20on%20Debian)](https://github.com/lemonade-sdk/lemonade/actions/workflows/linux_distro_builds.yml) | | [![Docker](https://img.shields.io/badge/Docker-supported-2496ED?logo=docker&logoColor=white)](https://lemonade-server.ai/install_options.html#docker) | [![Build Container Image](https://img.shields.io/github/actions/workflow/status/lemonade-sdk/lemonade/build-and-push-container.yml?branch=main&label=Build%20Container%20Image)](https://github.com/lemonade-sdk/lemonade/actions/workflows/build-and-push-container.yml) | | [![Fedora 43](https://img.shields.io/badge/Fedora-43-294172?logo=fedora&logoColor=white)](https://lemonade-server.ai/install_options.html#fedora) | [![Build .rpm](https://img.shields.io/github/actions/workflow/status/lemonade-sdk/lemonade/cpp_server_build_test_release.yml?branch=main&label=Build%20.rpm)](https://github.com/lemonade-sdk/lemonade/actions/workflows/cpp_server_build_test_release.yml) | | [![macOS beta](https://img.shields.io/badge/macOS-beta-999999?logo=apple&logoColor=white)](https://lemonade-server.ai/install_options.html#macos) | [![Build .pkg](https://img.shields.io/github/actions/workflow/status/lemonade-sdk/lemonade/cpp_server_build_test_release.yml?branch=main&label=Build%20.pkg)](https://github.com/lemonade-sdk/lemonade/actions/workflows/cpp_server_build_test_release.yml) | | [![Snap](https://img.shields.io/badge/Snap-supported-82BEA0?logo=snapcraft&logoColor=white)](https://snapcraft.io/lemonade-server) | [![Build Snap](https://img.shields.io/github/actions/workflow/status/lemonade-sdk/lemonade-server-snap/snap-build.yaml?branch=main&label=Build%20Snap)](https://github.com/lemonade-sdk/lemonade-server-snap/actions/workflows/snap-build.yaml) | | [![Ubuntu 24.04+](https://img.shields.io/badge/Ubuntu-24.04%2B-E95420?logo=ubuntu&logoColor=white)](https://lemonade-server.ai/install_options.html#ubuntu) | [![Build Launchpad PPA](https://img.shields.io/github/actions/workflow/status/lemonade-sdk/lemonade/launchpad-ppa.yml?branch=main&label=Build%20Launchpad%20PPA)](https://github.com/lemonade-sdk/lemonade/actions/workflows/launchpad-ppa.yml) | | [![Windows 11](https://img.shields.io/badge/Windows-11-0078D6?logo=windows&logoColor=white)](https://lemonade-server.ai/install_options.html#windows) | [![Build .msi](https://img.shields.io/github/actions/workflow/status/lemonade-sdk/lemonade/cpp_server_build_test_release.yml?branch=main&label=Build%20.msi)](https://github.com/lemonade-sdk/lemonade/actions/workflows/cpp_server_build_test_release.yml) | ## Using the CLI To run and chat with Gemma: ``` lemonade run Gemma-4-E2B-it-GGUF ``` To code with Lemonade models: ``` lemonade launch claude ``` Multi-modality: ``` # image gen lemonade run SDXL-Turbo # speech gen lemonade run kokoro-v1 # transcription lemonade run Whisper-Large-v3-Turbo ``` To see available models and download them: ``` lemonade list lemonade pull Gemma-4-E2B-it-GGUF ``` To see the backends available on your PC: ``` lemonade backends ``` ## Model Library Model Manager Lemonade supports a wide variety of LLMs (**GGUF**, **FLM**, and **ONNX**), whisper, stable diffusion, etc. models across CPU, GPU, and NPU. Use `lemonade pull` or the built-in **Model Manager** to download models. You can also import custom GGUF/ONNX models from Hugging Face. **[Browse all built-in models โ†’](https://lemonade-server.ai/models.html)**
## Supported Configurations Lemonade supports multiple inference engines for LLM, speech, TTS, and image generation, and each has its own backend and hardware requirements.
Modality Engine Backend Device OS
Text generation llamacpp vulkan x86_64 CPU, AMD iGPU, AMD dGPU Windows, Linux
rocm Supported AMD ROCm iGPU/dGPU families* Windows, Linux
cpu x86_64 CPU Windows, Linux
metal Apple Silicon GPU macOS (beta)
system x86_64 CPU, GPU Linux
flm npu XDNA2 NPU Windows, Linux
ryzenai-llm npu XDNA2 NPU Windows
Speech-to-text whispercpp npu XDNA2 NPU Windows
vulkan x86_64 CPU Linux
cpu x86_64 CPU Windows, Linux
Text-to-speech kokoro cpu x86_64 CPU Windows, Linux
Image generation sd-cpp rocm Supported AMD ROCm iGPU/dGPU families* Windows, Linux
cpu x86_64 CPU Windows, Linux
To check exactly which recipes/backends are supported on your own machine, run: ``` lemonade backends ```
* See supported AMD ROCm platforms
Architecture Platform Support GPU Models
gfx1151 (STX Halo) Windows, Ubuntu Ryzen AI MAX+ Pro 395
gfx120X (RDNA4) Windows, Ubuntu Radeon AI PRO R9700, RX 9070 XT/GRE/9070, RX 9060 XT
gfx110X (RDNA3) Windows, Ubuntu Radeon PRO W7900/W7800/W7700/V710, RX 7900 XTX/XT/GRE, RX 7800 XT, RX 7700 XT
## Project Roadmap | Under Development | Under Consideration | Recently Completed | |---------------------------|-----------------------------|------------------------| | Native multi-modal tool calling | vLLM support | Embeddable binary release | | More whisper.cpp backends | Port app to Tauri | Image generation | | More SD.cpp backends | | Speech-to-text | | MLX support | | Text-to-speech | | | | Apps marketplace | ## Integrate Embeddable Lemonade in You Application Embeddable Lemonade is a binary version of Lemonade that you can bundle into your own app to give it a portable, auto-optimizing, multi-modal local AI stack. This lets users focus on your app, with zero Lemonade installers, branding, or telemetry. Check out the [Embeddable Lemonade guide](docs/embeddable/README.md). ## Connect Lemonade Server to Your Application You can use any OpenAI-compatible client library by configuring it to use `http://localhost:13305/v1` as the base URL. A table containing official and popular OpenAI clients on different languages is shown below. Feel free to pick and choose your preferred language. | Python | C++ | Java | C# | Node.js | Go | Ruby | Rust | PHP | |--------|-----|------|----|---------|----|-------|------|-----| | [openai-python](https://github.com/openai/openai-python) | [openai-cpp](https://github.com/olrea/openai-cpp) | [openai-java](https://github.com/openai/openai-java) | [openai-dotnet](https://github.com/openai/openai-dotnet) | [openai-node](https://github.com/openai/openai-node) | [go-openai](https://github.com/sashabaranov/go-openai) | [ruby-openai](https://github.com/alexrudall/ruby-openai) | [async-openai](https://github.com/64bit/async-openai) | [openai-php](https://github.com/openai-php/client) | ### Python Client Example ```python from openai import OpenAI # Initialize the client to use Lemonade Server client = OpenAI( base_url="http://localhost:13305/api/v1", api_key="lemonade" # required but unused ) # Create a chat completion completion = client.chat.completions.create( model="Gemma-4-E2B-it-GGUF", # or any other available model messages=[ {"role": "user", "content": "What is the capital of France?"} ] ) # Print the response print(completion.choices[0].message.content) ``` For more detailed integration instructions, see the [Integration Guide](./docs/server/server_integration.md). ## FAQ To read our frequently asked questions, see our [FAQ Guide](./docs/faq.md) ## Contributing Lemonade is built by the local AI community! If you would like to contribute to this project, please check out our [contribution guide](./docs/contribute.md). ## Maintainers This is a community project maintained by @amd-pworfolk @bitgamma @danielholanda @jeremyfowers @kenvandine @Geramy @ramkrishna2910 @sawansri @siavashhub @sofiageo @superm1 @vgodsoe, and sponsored by AMD. You can reach us by filing an [issue](https://github.com/lemonade-sdk/lemonade/issues), emailing [lemonade@amd.com](mailto:lemonade@amd.com), or joining our [Discord](https://discord.gg/5xXzkMu8Zk). ## Code Signing Policy Free code signing provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org). - **Committers and reviewers**: [Maintainers](#maintainers) of this repo - **Approvers**: [Owners](https://github.com/orgs/lemonade-sdk/people?query=role%3Aowner) **Privacy policy**: This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it. When the user requests it, Lemonade downloads AI models from [Hugging Face Hub](https://huggingface.co/) (see their [privacy policy](https://huggingface.co/privacy)). ## License and Attribution This project is: - Built with C++ (server) and React (app) with โค๏ธ for the open source community, - Standing on the shoulders of great tools from: - [ggml/llama.cpp](https://github.com/ggml-org/llama.cpp) - [ggml/whisper.cpp](https://github.com/ggerganov/whisper.cpp) - [ggml/stable-diffusion.cpp](https://github.com/leejet/stable-diffusion.cpp) - [kokoros](https://github.com/lucasjinreal/Kokoros) - [OnnxRuntime GenAI](https://github.com/microsoft/onnxruntime-genai) - [Hugging Face Hub](https://github.com/huggingface/huggingface_hub) - [OpenAI API](https://github.com/openai/openai-python) - [IRON/MLIR-AIE](https://github.com/Xilinx/mlir-aie) - and more... - Licensed under the [Apache 2.0 License](https://github.com/lemonade-sdk/lemonade/blob/main/LICENSE). - Portions of the project are licensed as described in [NOTICE.md](./NOTICE.md). lemonade-sdk-lemonade-dbde812/contrib/000077500000000000000000000000001516551144000177745ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/contrib/debian/000077500000000000000000000000001516551144000212165ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/contrib/debian/changelog.in000066400000000000000000000002071516551144000234740ustar00rootroot00000000000000lemonade (@@DEB_VERSION@@) @@DEB_CODENAME@@; urgency=medium * Automated build -- Launchpad robot @@DEB_DATE@@ lemonade-sdk-lemonade-dbde812/contrib/debian/control000066400000000000000000000035641516551144000226310ustar00rootroot00000000000000Source: lemonade Section: utils Priority: optional Maintainer: Mario Limonciello Build-Depends: cmake, debhelper-compat (= 13), dh-cmake, dh-cmake-compat (= 1), dh-sequence-cmake, fonts-katex, jq, libdrm-dev, libcli11-dev, libcpp-httplib-dev (>> 0.26), libcurl4-openssl-dev, libjs-katex, libssl-dev, libsystemd-dev, libwebsockets-dev, libzstd-dev, ninja-build, nlohmann-json3-dev, node-buffer, node-css-loader, node-highlight.js, node-html-webpack-plugin, node-markdown-it, node-markdown-it-texmath, node-process, node-react, node-react-dom, node-style-loader, node-ts-loader, node-typescript, node-webpack, node-webpack-cli, nodejs, pkgconf, quilt, Testsuite: autopkgtest-pkg-python Standards-Version: 4.7.3 Homepage: https://lemonade-server.ai/ Vcs-Browser: https://salsa.debian.org/debian/lemonade Vcs-Git: https://salsa.debian.org/debian/lemonade.git Package: lemonade-server Architecture: linux-any Multi-Arch: foreign Depends: ${shlibs:Depends}, ${misc:Depends}, fonts-katex, unzip, libatomic1 Recommends: ffmpeg, libxrt-npu2, Description: Local LLM serving with GPU and NPU acceleration server Lemonade helps users run local LLMs with the highest performance by configuring state-of-the-art inference engines for their NPUs and GPUs. . Lemonade works with model providers such as huggingface. This package provides the server. Package: lemonade-desktop Architecture: all Multi-Arch: foreign Depends: ${shlibs:Depends}, ${misc:Depends}, jq, lemonade-server, xdg-utils Recommends: chromium | www-browser Description: Local LLM serving with GPU and NPU acceleration web application Lemonade helps users run local LLMs with the highest performance by configuring state-of-the-art inference engines for their NPUs and GPUs. . Lemonade works with model providers such as huggingface. This package provides the web application. lemonade-sdk-lemonade-dbde812/contrib/debian/copyright000066400000000000000000000202431516551144000231520ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Source: https://github.com/lemonade-sdk/lemonade Upstream-Name: lemonade Upstream-Contact: lemonade@amd.com Files: * Copyright: 2024-2025 Advanced Micro Devices, Inc. (AMD) 2023 Groq Inc. Comment: Portions derived from TurnkeyML/MLAgility License: Apache-2.0 Files: .devcontainer/reinstall-cmake.sh Copyright: Microsoft Corporation License: Expat Comment: Script from Microsoft's devcontainer templates Files: docs/assets/favicon.ico docs/assets/logo.png docs/assets/logo_512.png docs/favicon.ico src/cpp/resources/static/favicon.ico Copyright: Microsoft Corporation License: Expat Comment: Icons derived from Microsoft Fluent Emoji (lemon emoji) Source: https://github.com/microsoft/fluentui-emoji Files: src/app/* Copyright: 2024-2025 Advanced Micro Devices, Inc. (AMD) License: Expat Comment: Electron application with dependencies: - axios (MIT) - highlight.js (BSD-3-Clause) - katex (MIT) - markdown-it (MIT) - markdown-it-texmath (MIT) - react and react-dom (MIT) Files: src/app/src/renderer/components/Icons.tsx Copyright: Cole Bemis 2013-2022, Lucide Contributors 2022 License: ISC Files: src/cpp/include/lemon/utils/aixlog.hpp Copyright: 2017-2021 Johannes Pohl License: Expat Files: src/cpp/include/lemon/amdxdna_accel.h Copyright: 2022-2024, Advanced Micro Devices, Inc. License: GPL-2.0 Files: debian/* Copyright: 2025 Mario Limonciello License: GPL-2+ License: Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at . http://www.apache.org/licenses/LICENSE-2.0 . Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. . On Debian systems, the complete text of the Apache License, Version 2.0 can be found in "/usr/share/common-licenses/Apache-2.0". License: Expat 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. License: ISC Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. . THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. License: GPL-2.0 This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. . This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this program. If not, see . On Debian systems, the complete text of the GNU General Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". License: BSD-2-clause Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: . 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. . THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. License: GPL-2+ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. . This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this package; if not, see https://www.gnu.org/licenses/. . On Debian systems, the full text of the GNU General Public License version 2 can be found in the file '/usr/share/common-licenses/GPL-2'. License: BSD-3-clause Copyright (c) 2018 Machine Zone, Inc. All rights reserved. . Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: . 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. . 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. . 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. . THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. lemonade-sdk-lemonade-dbde812/contrib/debian/gbp.conf000066400000000000000000000000501516551144000226300ustar00rootroot00000000000000[DEFAULT] debian-branch = debian/latest lemonade-sdk-lemonade-dbde812/contrib/debian/lemonade-desktop.install000066400000000000000000000001541516551144000260410ustar00rootroot00000000000000usr/share/applications/lemonade-web-app.desktop usr/bin/lemonade-web-app usr/share/pixmaps/lemonade-app.svg lemonade-sdk-lemonade-dbde812/contrib/debian/lemonade-install.1000066400000000000000000000012311516551144000245250ustar00rootroot00000000000000.TH LEMONADE-INSTALL "1" "November 2025" "lemonade-install" "User Commands" .SH NAME lemonade-install \- Installation utility for Lemonade LLM models and components .SH SYNOPSIS .B lemonade-install [\fI\,OPTIONS\/\fR] .SH DESCRIPTION lemonade-install is a utility for installing and managing Lemonade LLM models, drivers, and related components. It automates the setup process for optimal LLM serving performance. .SH OPTIONS Run \fBlemonade-install --help\fR for detailed usage information. .SH SEE ALSO .BR lemonade (1), .BR lemonade-server (1) .SH AUTHOR Lemonade SDK developers .SH REPORTING BUGS Report bugs at: https://github.com/lemonade-sdk/lemonade/issues lemonade-sdk-lemonade-dbde812/contrib/debian/lemonade-server.1000066400000000000000000000011441516551144000243700ustar00rootroot00000000000000.TH LEMONADE-SERVER "1" "November 2025" "lemonade-server" "User Commands" .SH NAME lemonade-server \- Local LLM serving server with GPU and NPU acceleration .SH SYNOPSIS .B lemonade-server [\fI\,OPTIONS\/\fR] .SH DESCRIPTION lemonade-server is the server component of the Lemonade LLM serving system. It provides high-performance local LLM serving with GPU and NPU acceleration support. .SH OPTIONS Run \fBlemonade-server --help\fR for detailed usage information. .SH SEE ALSO .BR lemonade (1) .SH AUTHOR Lemonade SDK developers .SH REPORTING BUGS Report bugs at: https://github.com/lemonade-sdk/lemonade/issues lemonade-sdk-lemonade-dbde812/contrib/debian/lemonade-server.install000066400000000000000000000001651516551144000257000ustar00rootroot00000000000000etc/ usr/bin/lemonade-server usr/bin/lemond usr/bin/lemonade usr/share/lemonade/ usr/share/lemonade-server/ usr/lib/ lemonade-sdk-lemonade-dbde812/contrib/debian/lemonade-server.manpages000066400000000000000000000000251516551144000260200ustar00rootroot00000000000000usr/share/man/man1/* lemonade-sdk-lemonade-dbde812/contrib/debian/lemonade-server.postinst000066400000000000000000000014441516551144000261160ustar00rootroot00000000000000#!/bin/sh set -e if [ "$1" = "configure" ]; then legacy_home="/opt/var/lib/lemonade" new_home="/var/lib/lemonade" # Create the lemonade user and group if they don't exist if ! getent group lemonade > /dev/null; then groupadd lemonade fi if ! getent passwd lemonade > /dev/null; then useradd -m -r -g lemonade -d "$new_home" -s /usr/sbin/nologin lemonade fi # Move all data from the legacy home if [ -d "$legacy_home" ]; then usermod -d "$new_home" -m lemonade >/dev/null fi # Add lemonade user to systemd-journal group for journal access. if getent group systemd-journal > /dev/null; then usermod -a -G systemd-journal lemonade fi # Add lemonade user to render group for GPU access if getent group render > /dev/null; then usermod -a -G render lemonade fi fi #DEBHELPER# lemonade-sdk-lemonade-dbde812/contrib/debian/lemonade-server.postrm000066400000000000000000000003121516551144000255500ustar00rootroot00000000000000#!/bin/sh set -e if [ purge = "$1" ]; then # Handle the rare case "userdel" not being available, see bug #1071142. if command -v userdel > /dev/null 2>&1; then userdel lemonade fi fi #DEBHELPER# lemonade-sdk-lemonade-dbde812/contrib/debian/lemonade-server.preinst000066400000000000000000000005321516551144000257140ustar00rootroot00000000000000#!/bin/sh set -e # For migrating systemd service from /opt to /usr if [ -L "/usr/lib/systemd/system/lemonade-server.service" ]; then deb-systemd-helper purge 'lemonade-server.service' >/dev/null || true rm -f /etc/systemd/system/lemonade-server.service rm -f /etc/systemd/system/*/lemonade-server.service systemctl --system daemon-reload fi lemonade-sdk-lemonade-dbde812/contrib/debian/lemond.1000066400000000000000000000011151516551144000225540ustar00rootroot00000000000000.TH LEMOND "1" "November 2025" "lemond" "User Commands" .SH NAME lemond \- Request router for Lemonade LLM serving system .SH SYNOPSIS .B lemond [\fI\,OPTIONS\/\fR] .SH DESCRIPTION lemond handles request routing and load balancing for the Lemonade LLM serving system. It distributes requests across available LLM instances for optimal performance. .SH OPTIONS Run \fBlemond --help\fR for detailed usage information. .SH SEE ALSO .BR lemonade (1), .BR lemonade-server (1) .SH AUTHOR Lemonade SDK developers .SH REPORTING BUGS Report bugs at: https://github.com/lemonade-sdk/lemonade/issues lemonade-sdk-lemonade-dbde812/contrib/debian/rules000077500000000000000000000013331516551144000222760ustar00rootroot00000000000000#!/usr/bin/make -f DEB_BUILD_DIR := obj-$(DEB_HOST_GNU_TYPE) %: dh $@ --buildsystem=cmake+ninja override_dh_auto_configure: dh_auto_configure -- -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WEB_APP=ON -DUSE_SYSTEM_NODEJS_MODULES=ON override_dh_auto_test: # No tests configured override_dh_install: dh_install # On Ubuntu, prefer system llama.cpp by default ifeq (yes,$(shell dpkg-vendor --derives-from Ubuntu 2>/dev/null && echo yes)) jq '.llamacpp.prefer_system = true' debian/lemonade-server/usr/share/lemonade/defaults.json \ > debian/lemonade-server/usr/share/lemonade/defaults.json.tmp && \ mv debian/lemonade-server/usr/share/lemonade/defaults.json.tmp \ debian/lemonade-server/usr/share/lemonade/defaults.json endif lemonade-sdk-lemonade-dbde812/contrib/debian/salsa-ci.yml000066400000000000000000000007171516551144000234420ustar00rootroot00000000000000# For more information on what jobs are run see: # https://salsa.debian.org/salsa-ci-team/pipeline # # To enable the jobs, go to your repository (at salsa.debian.org) # and click over Settings > CI/CD > Expand (in General pipelines). # In "CI/CD configuration file" write debian/salsa-ci.yml and click # in "Save Changes". The CI tests will run after the next commit. --- include: - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml lemonade-sdk-lemonade-dbde812/contrib/debian/source/000077500000000000000000000000001516551144000225165ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/contrib/debian/source/format000066400000000000000000000000151516551144000237250ustar00rootroot000000000000003.0 (native) lemonade-sdk-lemonade-dbde812/contrib/debian/source/options000066400000000000000000000000521516551144000241310ustar00rootroot00000000000000extend-diff-ignore = "^[^/]*[.]egg-info/" lemonade-sdk-lemonade-dbde812/contrib/debian/upstream/000077500000000000000000000000001516551144000230565ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/contrib/debian/upstream/metadata000066400000000000000000000003631516551144000245630ustar00rootroot00000000000000Bug-Database: https://github.com/lemonade-sdk/lemonade/issues Bug-Submit: https://github.com/lemonade-sdk/lemonade/issues/new Repository-Browse: https://github.com/lemonade-sdk/lemonade Repository: https://github.com/lemonade-sdk/lemonade.git lemonade-sdk-lemonade-dbde812/contrib/debian/watch000066400000000000000000000001021516551144000222400ustar00rootroot00000000000000Version: 5 Template: Github Owner: lemonade-sdk Project: lemonade lemonade-sdk-lemonade-dbde812/contrib/launchpad-downloads/000077500000000000000000000000001516551144000237235ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/contrib/launchpad-downloads/README.md000066400000000000000000000042351516551144000252060ustar00rootroot00000000000000# Launchpad PPA Download Statistics Tool A Python tool to retrieve download statistics for packages in Launchpad PPAs (Personal Package Archives). ## Installation 1. Install dependencies: ```bash pip install -r requirements.txt ``` Or install directly: ```bash pip install launchpadlib ``` ## Usage Basic usage: ```bash python ppa_stats.py username/ppa-name ``` ### Examples Get stats for all packages in a PPA: ```bash python ppa_stats.py developmentseed/mapbox ``` Get stats for a specific package: ```bash python ppa_stats.py developmentseed/mapbox -p tilemill ``` Show all versions including those with 0 downloads: ```bash python ppa_stats.py username/ppa-name --all ``` Show summary grouped by package name: ```bash python ppa_stats.py username/ppa-name --summary ``` ### Options - `-p, --package PACKAGE` - Query a specific package name - `--all` - Show all versions including those with 0 downloads - `--summary` - Show summary by package name instead of individual versions ## How It Works The tool uses the Launchpad API via the `launchpadlib` Python library to: 1. Connect anonymously to Launchpad's production API 2. Retrieve the specified PPA for the given user/team 3. Fetch published binary packages and their download counts 4. Display formatted statistics ## API Documentation The tool uses Launchpad's public API: - API endpoint: https://api.launchpad.net/ - No authentication required for read-only access - Uses the `getDownloadCount()` method on published binaries ## Output Format The tool displays: - Total downloads across all packages/versions - Individual package versions with download counts - Architecture information for each binary package - Sorted by download count (most popular first) ## Notes - Download statistics are provided by Launchpad and may have some delay - The tool caches API responses in `~/.launchpadlib/cache/` for performance - Anonymous access is sufficient for read-only statistics ## Sources Based on information from: - [Launchpad API Documentation](https://api.launchpad.net/) - [PPA Download Statistics Examples](https://gist.github.com/springmeyer/2778600) - [ppa-stats GitHub Project](https://github.com/hsheth2/ppa-stats) lemonade-sdk-lemonade-dbde812/contrib/launchpad-downloads/ppa_stats.py000066400000000000000000000112451516551144000262760ustar00rootroot00000000000000#!/usr/bin/env python3 """ Launchpad PPA Download Statistics Tool This tool retrieves download statistics for packages in a Launchpad PPA. """ import os import sys import argparse from launchpadlib.launchpad import Launchpad def get_ppa_stats(username, ppa_name, package_name=None, show_all=False): """ Get download statistics for a Launchpad PPA. Args: username: Launchpad username/team name ppa_name: Name of the PPA package_name: Optional specific package name to query show_all: Show all versions including those with 0 downloads Returns: Total downloads and list of package statistics """ # Setup cache directory cachedir = os.path.expanduser("~/.launchpadlib/cache/") print(f"Connecting to Launchpad API...") launchpad = Launchpad.login_anonymously( "ppa-stats-tool", "production", cachedir, version="devel" ) # Get the PPA try: print(f"Fetching PPA: {username}/{ppa_name}") ppa = launchpad.people[username].getPPAByName(name=ppa_name) except Exception as e: print(f"Error: Could not find PPA '{ppa_name}' for user '{username}'") print(f"Details: {e}") sys.exit(1) # Get published binaries if package_name: print(f"Fetching statistics for package: {package_name}") bins = ppa.getPublishedBinaries(binary_name=package_name) else: print(f"Fetching statistics for all packages...") bins = ppa.getPublishedBinaries() # Collect download counts builds = [] total_downloads = 0 for binary in bins: count = binary.getDownloadCount() total_downloads += count if count > 0 or show_all: builds.append( { "count": count, "name": binary.binary_package_name, "version": binary.binary_package_version, "arch": binary.distro_arch_series_link.split("/")[-1], } ) # Sort by download count (descending) builds_sorted = sorted(builds, key=lambda x: x["count"], reverse=True) return total_downloads, builds_sorted def print_stats(total_downloads, builds, show_details=True): """Print download statistics in a formatted way.""" print("\n" + "=" * 70) print(f"TOTAL DOWNLOADS: {total_downloads:,}") print("=" * 70) if not builds: print("No packages found or no downloads recorded.") return if show_details: print(f"\n{'Downloads':<12} {'Package':<30} {'Version':<20} {'Arch':<10}") print("-" * 70) for build in builds: print( f"{build['count']:<12,} {build['name']:<30} {build['version']:<20} {build['arch']:<10}" ) else: # Group by package name package_totals = {} for build in builds: name = build["name"] if name not in package_totals: package_totals[name] = 0 package_totals[name] += build["count"] print(f"\n{'Downloads':<12} {'Package':<30}") print("-" * 42) for name, count in sorted( package_totals.items(), key=lambda x: x[1], reverse=True ): print(f"{count:<12,} {name:<30}") def main(): parser = argparse.ArgumentParser( description="Get download statistics for a Launchpad PPA", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s username/ppa-name %(prog)s developmentseed/mapbox -p tilemill %(prog)s myuser/myppa --all --summary """, ) parser.add_argument("ppa", help="PPA in format: username/ppa-name") parser.add_argument( "-p", "--package", help="Specific package name to query (optional)" ) parser.add_argument( "--all", action="store_true", help="Show all versions including those with 0 downloads", ) parser.add_argument( "--summary", action="store_true", help="Show summary by package name instead of individual versions", ) args = parser.parse_args() # Parse PPA format if "/" not in args.ppa: print("Error: PPA must be in format 'username/ppa-name'") sys.exit(1) username, ppa_name = args.ppa.split("/", 1) try: total, builds = get_ppa_stats(username, ppa_name, args.package, args.all) print_stats(total, builds, show_details=not args.summary) except KeyboardInterrupt: print("\n\nInterrupted by user") sys.exit(130) except Exception as e: print(f"\nError: {e}") import traceback traceback.print_exc() sys.exit(1) if __name__ == "__main__": main() lemonade-sdk-lemonade-dbde812/contrib/launchpad-downloads/requirements.txt000066400000000000000000000000251516551144000272040ustar00rootroot00000000000000launchpadlib>=1.10.0 lemonade-sdk-lemonade-dbde812/data/000077500000000000000000000000001516551144000172455ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/data/lemonade-app.desktop000066400000000000000000000004401516551144000232000ustar00rootroot00000000000000[Desktop Entry] Version=1.0 Type=Application Name=Lemonade App Comment=Local LLMs with GPU and NPU acceleration - Desktop Application GenericName=AI Model Manager Exec=lemonade-app Icon=lemonade-app Terminal=false Categories=Development;Utility; Keywords=AI;LLM;GPU;NPU;Machine Learning; lemonade-sdk-lemonade-dbde812/data/lemonade-server.service.in000066400000000000000000000017151516551144000243300ustar00rootroot00000000000000[Unit] Description=Lemonade Server After=network-online.target [Service] Type=simple User=lemonade Group=lemonade WorkingDirectory=@CMAKE_INSTALL_FULL_LOCALSTATEDIR@/lib/lemonade # Server settings (port, host, models_dir, โ€ฆ) live in config.json, not env vars. # The env-file drop-in is kept for the few env vars lemond still reads: # HF_TOKEN โ€” HuggingFace authentication token # LEMONADE_API_KEY โ€” require API-key auth on all routes # LEMONADE_ADMIN_API_KEY โ€” API-key specific to internal routes EnvironmentFile=-/etc/lemonade/conf.d/*.conf ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/lemond Restart=on-failure RestartSec=5s KillSignal=SIGINT AmbientCapabilities=CAP_SYS_RESOURCE # Security hardening PrivateTmp=yes NoNewPrivileges=yes ProtectSystem=full ProtectHome=yes ReadWritePaths=@CMAKE_INSTALL_FULL_LOCALSTATEDIR@/lib/lemonade RestrictRealtime=yes RestrictNamespaces=yes LockPersonality=yes [Install] WantedBy=multi-user.target lemonade-sdk-lemonade-dbde812/data/lemonade-web-app000066400000000000000000000016741516551144000223150ustar00rootroot00000000000000#!/bin/bash set -e LEMONADE_PORT="${LEMONADE_PORT:-13305}" # Discover the actual port from a running server if command -v lemonade &> /dev/null; then DISCOVERED_PORT=$(lemonade status --json 2>/dev/null | jq -r '.port // empty') if [ -n "$DISCOVERED_PORT" ]; then LEMONADE_PORT="$DISCOVERED_PORT" fi fi URL="http://localhost:${LEMONADE_PORT}/lemonade" # Prefer chromium based browsers otherwise xdg-open if [ "$(echo "${LEMONADE_PREFER_CHROMIUM:-true}" | tr '[:upper:]' '[:lower:]')" = "true" ]; then if command -v google-chrome &> /dev/null; then google-chrome --app="$URL" elif command -v microsoft-edge-stable &> /dev/null; then microsoft-edge-stable --app="$URL" elif command -v chromium &> /dev/null; then chromium --app="$URL" elif command -v chromium-browser &> /dev/null; then chromium-browser --app="$URL" else xdg-open "$URL" fi else xdg-open "$URL" fi lemonade-sdk-lemonade-dbde812/data/lemonade-web-app.desktop000066400000000000000000000005241516551144000237560ustar00rootroot00000000000000[Desktop Entry] Version=1.0 Type=Application Name=Lemonade Web App Comment=Local LLMs with GPU and NPU acceleration - Web Interface GenericName=AI Model Manager Exec=lemonade-web-app Icon=lemonade-app Terminal=false Categories=Development;Utility; Keywords=AI;LLM;GPU;NPU;Machine Learning; StartupWMClass=chrome-localhost__lemonade-Default lemonade-sdk-lemonade-dbde812/data/secrets.conf000066400000000000000000000002401516551144000215600ustar00rootroot00000000000000# Installed as /etc/lemonade/conf.d/zz-secrets.conf so it loads after other # drop-ins and keeps secrets separate from the base config file. #LEMONADE_API_KEY= lemonade-sdk-lemonade-dbde812/docs/000077500000000000000000000000001516551144000172645ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/docs/CNAME000066400000000000000000000000231516551144000200250ustar00rootroot00000000000000lemonade-server.ai lemonade-sdk-lemonade-dbde812/docs/README.md000066400000000000000000000010541516551144000205430ustar00rootroot00000000000000# Lemonade SDK Documentation This documentation has moved to our main documentation site. ## Lemonade Server For the Lemonade Server (OpenAI-compatible HTTP server for local LLMs), see: **[Lemonade Server Documentation โ†’](https://lemonade-server.ai/docs/)** ## lemonade-eval CLI For the `lemonade-eval` CLI (benchmarking, accuracy evaluation, and model preparation), see: **[lemonade-eval Documentation โ†’](./eval/README.md)** lemonade-sdk-lemonade-dbde812/docs/assets/000077500000000000000000000000001516551144000205665ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/docs/assets/carousel.js000066400000000000000000000042651516551144000227500ustar00rootroot00000000000000// Simple YouTube video carousel for MkDocs Material document.addEventListener('DOMContentLoaded', function () { var carousel = document.getElementById('yt-carousel'); if (!carousel) return; // Support both data-ids (comma-separated) and data-videos (JSON array of {id, title}) var videos = []; if (carousel.dataset.videos) { try { videos = JSON.parse(carousel.dataset.videos); } catch (e) { console.error('Invalid JSON in data-videos:', e); } } else if (carousel.dataset.ids) { videos = carousel.dataset.ids.split(',').map(function(id) { return { id: id.trim(), title: '' }; }); } if (!videos.length) return; var idx = 0; function render() { var video = videos[idx]; var titleHtml = video.title ? `
${video.title}
` : ''; carousel.innerHTML = `
${titleHtml}
${idx+1} / ${videos.length}
`; document.getElementById('yt-prev').onclick = function() { idx = (idx - 1 + videos.length) % videos.length; render(); }; document.getElementById('yt-next').onclick = function() { idx = (idx + 1) % videos.length; render(); }; } render(); }); lemonade-sdk-lemonade-dbde812/docs/assets/common.css000066400000000000000000000124551516551144000225770ustar00rootroot00000000000000@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); :root { --docs-page-title-color: #1a1a1a; --docs-page-subtitle-color: #444444; --docs-side-text: #6b6148; --docs-side-text-strong: #3a301a; --docs-side-label: #6d613f; --docs-side-border: rgba(79, 66, 30, 0.18); --docs-side-surface: rgba(255, 255, 255, 0.55); --docs-side-surface-hover: rgba(255, 255, 255, 0.62); --docs-side-active-bg: rgba(46, 39, 25, 0.9); --docs-side-active-text: #fffef7; } /* Shared top navigation */ .navbar, .navbar a, .navbar .brand-title { font-family: 'Inter', 'Segoe UI', 'Arial', sans-serif; } /* Shared non-index page background */ #models-page-body, #news-page-body, #marketplace-body { background: radial-gradient(circle at top, rgba(255, 237, 172, 0.12), transparent 42%), #fffbe6 !important; } /* Remove pre-footer hairline on docs subpages */ #models-page-body .site-footer, #news-page-body .site-footer, #marketplace-body .site-footer { border-top: none; } /* Shared non-homepage heading system (matches news style) */ .docs-page-hero { text-align: center; } .docs-page-title { margin: 0 0 16px; font-size: 3.5rem; font-weight: 400; color: var(--docs-page-title-color); line-height: 1.05; letter-spacing: -0.03em; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .docs-page-subtitle { margin: 0 auto; max-width: 700px; font-size: 1.125rem; font-weight: 500; font-family: 'Inter', 'Segoe UI', 'Arial', sans-serif; color: var(--docs-page-subtitle-color); line-height: 1.6; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } [data-theme="dark"] .docs-page-title { color: #ffffff; text-shadow: none; } [data-theme="dark"] .docs-page-subtitle { color: #cccccc; text-shadow: none; } @media (max-width: 825px) { .docs-page-title { font-size: clamp(2.2rem, 7vw, 3rem); text-align: center; } .docs-page-subtitle { font-size: clamp(1rem, 2.3vw, 1.125rem); max-width: 680px; text-align: center; } } @media (max-width: 640px) { .docs-page-title { font-size: clamp(1.95rem, 8.2vw, 2.4rem); } .docs-page-subtitle { max-width: 42ch; } } /* Shared mobile docs sidebar toggle (Models + News) */ .docs-side-toggle { display: inline-flex; align-items: center; gap: 9px; border: 1px solid rgba(43, 35, 19, 0.11); background: #fff; color: #232323; border-radius: 10px; padding: 8px 12px; font-size: 0.87rem; font-weight: 600; width: fit-content; cursor: pointer; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06); } .docs-side-toggle:hover { box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06); transform: none; } .docs-side-toggle .hamburger-icon { display: inline-flex; flex-direction: column; justify-content: center; gap: 3px; width: 14px; } .docs-side-toggle .hamburger-icon span { display: block; width: 100%; height: 2px; border-radius: 999px; background: #282828; } @media (max-width: 720px) { .docs-side-toggle { padding: 8px 11px; } } /* Shared side navigation visual language */ .docs-side-nav { color: var(--docs-side-text); } .docs-side-nav .sidebar-card { border: none; border-bottom: 1px solid var(--docs-side-border); border-radius: 0; background: transparent; padding: 0 0 14px; margin-bottom: 14px; box-shadow: none; } .docs-side-nav .sidebar-card:last-child { border-bottom: none; margin-bottom: 0; } .docs-side-nav .control-label { display: block; margin: 0 0 8px; font-size: 0.74rem; font-weight: 650; letter-spacing: 0.05em; text-transform: uppercase; color: var(--docs-side-label); } .docs-side-nav .models-search-input, .docs-side-nav .sort-select { border: 1px solid var(--docs-side-border); border-radius: 8px; background: var(--docs-side-surface); box-shadow: none; } .docs-side-nav .models-search-input:focus, .docs-side-nav .sort-select:focus { border-color: rgba(107, 90, 42, 0.44); box-shadow: 0 0 0 3px rgba(189, 164, 92, 0.14); } .docs-side-nav .backend-filter-btn, .docs-side-nav .filter-btn { display: block; width: 100%; text-align: left; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--docs-side-text); box-shadow: none; padding: 6px 8px; font-size: 0.8rem; font-weight: 500; transition: background 0.15s ease, color 0.15s ease; } .docs-side-nav .backend-filter-btn:hover, .docs-side-nav .filter-btn:hover { color: var(--docs-side-text-strong); background: var(--docs-side-surface-hover); border-color: transparent; box-shadow: none; transform: none; } .docs-side-nav .backend-filter-btn.active, .docs-side-nav .backend-filter-btn.active:hover, .docs-side-nav .filter-btn.active, .docs-side-nav .filter-btn.active:hover { background: var(--docs-side-active-bg); color: var(--docs-side-active-text); border-color: transparent; box-shadow: none; transform: none; } .docs-side-nav .label-filter-btn { border: 1px solid var(--docs-side-border); border-radius: 6px; background: transparent; color: #64593f; } .docs-side-nav .label-filter-btn:hover { border-color: rgba(79, 66, 30, 0.28); color: var(--docs-side-text-strong); background: var(--docs-side-surface-hover); } .docs-side-nav .label-filter-btn.active { border-color: transparent; background: var(--docs-side-active-bg); color: var(--docs-side-active-text); } lemonade-sdk-lemonade-dbde812/docs/assets/extra.css000066400000000000000000000056401516551144000224300ustar00rootroot00000000000000/* Note: I have not figured out all the color variables yet */ [data-md-color-scheme="lightmode"] { --md-primary-fg-color: #FFFBE9; /* Header, Selected Page Font */ --md-primary-bg-color: #000000; /* Header Font, Icon Color*/ --md-primary-bg-color--light: #000000; /* Search bar font color */ --md-accent-fg-color: #FFD744; /* Hover color of links */ --md-footer-fg-color: #E59800; /* Nav Footer Font Color */ --md-footer-fg-color--light: #3b3b3b; /* Footer Font Color */ --md-footer-fg-color--lighter: #3b3b3b; /* Made With... color */ --md-footer-bg-color: #FFFBE9; /* Nav Footer Background Color */ --md-footer-bg-color--dark: #FFFBE9; /* Footer Background Color */ --md-default-bg-color: #FFFBE9; /* Main background color */ --md-code-bg-color: #ffefb5; /* Code block background color */ --md-code-fg-color: #000000; /* Code block font color */ --md-default-fg-color--light: #E59800; /* Blockquote color */ } [data-md-color-scheme="slate"] { --md-primary-fg-color: #FFD500; /* Header, Selected Page Font */ --md-primary-bg-color: #000000; /* Header Font, Icon Color*/ --md-primary-bg-color--light: #000000; /* Search bar font color */ --md-accent-fg-color: #FFD500; /* Hover color of links */ --md-accent-fg-color--transparent: #E59800; --md-footer-fg-color: #E59800; /* Nav Footer Font Color */ --md-footer-fg-color--light: #929292; /* Footer Font Color */ --md-footer-fg-color--lighter: #929292; /* Made With... color */ --md-footer-bg-color: #000000; /* Nav Footer Background Color */ --md-footer-bg-color--dark: #000000; /* Footer Background Color */ --md-primary-bg-color--light: #000000; /* Search Font */ } [data-md-color-scheme="slate"] { --md-hue: 320; /* between 0 and 360 */ /* --md-saturation: 50; /* between 0 and 100 */ /* --md-lightness: 100; between 0 and 100 */ --md-footer-bg-color: #141413; /* Nav Footer Background Color */ --md-default-bg-color: #141413 !important; /* Dark background */ --md-primary-fg-color: #E59800 !important; /* Header, Selected Page Font */ --md-footer-bg-color--dark: #1f1503 !important; /* Footer Background Color */ } .hide-in-mkdocs { display: none; } /* docs/assets/extra.css */ .mkdocs-only { display: block; } /* Hide the page title in the navigation sidebar */ .md-nav__title { display: none !important; } /* Make page titles (h1) a darker grey in light mode, lighter in dark mode */ [data-md-color-scheme="lightmode"] h1, [data-md-color-scheme="lightmode"] .md-typeset h1 { color: #222 !important; } [data-md-color-scheme="slate"] h1, [data-md-color-scheme="slate"] .md-typeset h1 { color: #cfcfcf !important; } lemonade-sdk-lemonade-dbde812/docs/assets/favicon.ico000066400000000000000000003674311516551144000227250ustar00rootroot00000000000000 hf  จฮ00 จ%v@@ (B;€€ (F} ซin…(  Ÿงฌถ€”๕~…่ m—๘ะ>ดมnฎ˜คŽž8Š“P“๚ph˜๙ฤU้ @†าDˆึ‚ถˆด€นF|ณพuจ๑p›k๙_‰๐K‡แ๕B‚ฮB@„าะ1tjบqษlรJpฦๆoยjถdจ^›W๓SƒเUwฬฃYlณ WqปYศXฦaฮฮjำkหeฟ`ณYซMฃB›๐?๙@ƒว`@‡ิOล`ใQฮz_ืfุ`ฮZรXผQธDต7ฑ0ฉ๐1šุต:‰ธ FหCษPีฮ`^ูTฯPวQฤMฤAร4ย-พ-ฐ๊ึ5ŸะBฯAฮ@Pฺ๔^เTูIัJอPฮQะHั:ะ0ฬ0พ๔ั9ฌฺ=า=ฯ๚ZMWแIฺAิJิWุ[RBฺ5ิ4ฦ๗ซHฃอ6ฮ๖6ฬ๔NBฺ๚Kโ>๋ะG์ฅM่KNูM–fพ‘%ศ_4ิซบLโปูV๊ลฌK๊ม3<ท๏?ำ๖>ื๚?อ๙A๛">็$ต†=ไ)ฮข 9ฒPไพ @ใปtบว๘๘๐๐เเเเเภ€€ใ( @ ๒ฃ๒Šํ•~ใd›๛์Pฟฅิœ๛Šž&yžŒn–๘สgŠ๋ฎg?!psฦฟ›}กข˜ŸA“›r•๛—ŒŽ๘ก{–๛i›_š๚R”๐H‹฿ีD„อ!E…ะึ‘ฒ•ฏŽฎj‡ซศ€ฆ๔zกv›t”t๚o‰๕dŠ๓Z๐K๋9฿4‰ฯU4‹ำ‘ฎป…ท+ธดzธ๛sตnฏkฉhขf›d”cŒ๗a…๏\่V€แIุAษ€6‹แXqฐฑwพwผ-vฟหrยoยnพlทiฐfจcก`›]•๛ZŽ๔X‡์X€โZxื^rศัgkป&enฟtaจjภkฟkฤผkษmหoษnฤlฝhตeฎbจ_ข[V˜๙R“๒NŒ๊M„เRzาXrยฏZlฒYnทaภ`ธaลŠcฬiัmัnฮlศiมfบcณaฎ]ฉXฅRกM๘G˜๑C“้B‹E€อJxฝuEŽKtฐ]พXวXล?Zฬ์bิiึlิjะgษcรaฝ`ธ^ดZฐUญNชGงAฃ๗<Ÿ๑9š่8‘<†ศูA}ธ!@บSฤSภRสชYำcูiูhึeัaส^ล\ฟ[ปZธVถQดJฒBฐ;ญ6ซ๘3ง๑0 ็2•ึ8Šรf5Žห?„ตqตๆMษMศAPะ๐[ูdfฺcึ_ัZหWฦWยVฟUฝRผLบEน>ธ7ท2ต/ฒ๙-ญ๑,คใ2—ฯฅNx™<ฟNล๛Sฏ๕Iฬ™Qึ_dc^ึYัTฬRศRลRรQยOมJมCภ<ฟ6ฟ1พ.ผ,ธ๚+ฐํ0ฃูษ;”ม:˜วGศFว GะTฺ`฿b_YึSัOอMษNวOวPฦNวKวEว>ฦ7ฦ2ล/ฤ-ม,บ๕0ฌแี;žห; ฯBฬBสRFิ๚W`เ`฿ZSืMาJฮJหLสNหPหPอNอIฮBอ;อ5ฬ1ห.ศ-ม๚2ด็ฯ?คะ=งิHร๐:ึ?ฬ‚GืX฿^แ\฿UMืGำEะGฮKฮOฯSัTำSิNิGิ@ำ9า3ั/อ.ฦ๛4ธ๊ธIคหBฌืFรํgp›=อ๛กGูWเ\โXเPGุBิBาFัLาRิVึYุXฺSฺMฺDู=ุ6ี1า0ศ๛8ป๋IฏุCย๊Iฌำ:อ๙ชDูTแXใSเJBู>ึ@ีFีOืWฺ\^]฿X฿P฿H?8ู3ิ3ษ๙๙=ผ๊Q:ฟ๎Bมๆฎ7ห๕ž>ุMแRใMแE=:ู?ุHฺS]เcใeๅbๅ\ไSใJโA฿:4ี8ศ๗ิDปๆCฝ่Aฝเ.ะ๚5ว๎{7ิ๛C฿IใFโ>เ87>J฿Xโcๆj๊l๋h๋`๊V่LๅCโ;7ิ=ว๔ˆKผไ4ร็5มๅC1ฮ๕๔8>แ=โ8โ4แ5เ=แKไ[่h์p๐q๒m๑e๏Z์O้Dๅ;;ั๛ๆDล๑1Bว๓8ผ9ธู0ว๋ภ/ึ๛3฿4โ2ใ0ไ2ไ:ๆI้Zํh๒q๕t๗p๖h๔]๑Q์Eๆ>Aฯ๙‘`ชำLร์>ณา+ษ์2ภแ^+ะ๓๙+-แ-ไ-ๅ/็6้C์S๐b๕m๙q๛p๚h๘^๔R๎FๆCุูHห๖(Gฮ๗ˆ‚คamnŸeฉ•_ฌ‘ ?ฑี -ษ๊ฆ'ุ๙*เ,ใ-ๆ.้1๋;๎H๒W๖d๚kkf๛]๖Q๎Hแ๔Iิ๛_:๎Rศ๐]vhKฎ‘Lจ‹Nญ’pQฑ—ธOทœฮ=ฝŸฟ%มŸ รนR&ัํย(/โ0ๆ0้1์5๏>๓J๖W๚_c`๛W๕N่๘L€Sว๔Pะ๗Gฃƒ>ฏŒ?ญŠ!=ฒด;ท–<ผœ@มก>ลฅ0ษงฬงๅฮฒนึ฿แ/฿๚8ไ7่5์5๏9๒A๕J๘R๚V๚T๕O๋๎NเyQา๙ Pื๛2ฐ†3ฎƒ 0ตŒช+บ“,ภš1ล 9ษฆ?ฮซ?ัฎ5ิฐ'ืฐูด๙ อ5฿ํ๗?๗<ไ9้8ํ;๏@๑E๐Iํ๓K็ถNเGSั๚Qุ๛*ฐ…,ฎ‚ "ท‹~!ฟ”%ล›,หข8ะฉBีฐGูตEท?น9โผ4ๆพ๛/โฤ„Bะ๑LAึ๖=๚บ:฿ฬ;แษ>โฑCแ€G฿@Lุ๛ GไUบ๓'ธ‹*ฒ…$ฝ‘#$ร—x$ษžำ,ัง>ูฒPแปX็ยZ๋ฦXํศ๔Q๋ลŒEใฝ HโฟS™ปFรๅCอ๐Dฯ๓Kษ๐๏wjšุ;ธf“k,ร—)หŸj.ิฉบ=ดฺOๆฟาY๊ลคZ๊ฦLUไภW็รJฑ‰0ศ +ะฆ.ำช'สข2ูฒa0๐?๘๐เภ€€๘เภ?€€ภ๘(0` $Uด”๔ซ‰์ž†้ ฉzŽ๓S”๛Ÿœ๚ˆH{™๚ฃs’๔ฤqˆ๊ฆu~O…rฮ~vิ๑…๛jภฏ‘๛ช๙ฉ๗ฆ๗”—๛9‚œt ๑iŸa™๚[’๑Yˆๅ๔]€ืzarถ^yวธฯ™คกœŸG˜”šญ‘–ฦ“ั๛าŽ๘ี†๙๎t–gšbš]™๙S–๓F“๊B‹้Dƒห4D„อฆฉ๛คšซ!”ชzŽงอ‡ฅ๖ข|Ÿx›v—v’v๚vŠ๖q‰๔f‹๔^Ž๔X‘๒N’๏A“้66‰ฮm3Œึ’ฑ•ฐ Žฒb‰ฒึ€ฑyฏtฌpจnคl k›j–i‘iŒ๘i‡๔e„๏_‚์X„้Q‡็E‹ใ7Œ4‡ฬ“จ=€บ’ช‡ถˆต…ท”~น๗vบrนoถmฒkญiจgคfŸe›c–b‘๛aŒ๖`ˆ๑_ƒ์\ๆY|แV|KีC}ศพHuณ Iwน}บ~ธ|ผฆvฟqมpมoฟnผlทjฒhญfจeฃcŸa›_–]’๙\Ž๕Z‰๏Y„๊Yใ[z^uำ_sศ๒doฝOXvวpgฒsผtป sฟ›pรmวnวoวoฤnภmผjทhฑfฌdจbฃ`Ÿ^œ[˜Y”๘V๓TŒ๎R‡่S‚แU|ูZuฯboยีfjท1cmพzY™kฝlฒkภwiล๚iสkฬnอoหoศnฤlฟjบhตfฐdฌbจ_ค]กZVš๛S—๖P“๒MํL‹็K†เLืRwหYqพฝ[lฒZmตrฒcรdมBcฦ้cฬgะlัnะoฮnสlฦjมhฝfธdดcฐaฌ^ฉ\ฆXฃT P๚Lš๖I—๑F”์DๆCŠ฿D„ีJ{วOtบ†WeŽRpญ^ย_ภ]ฦป^ฬbาhิlิmำmะkฬiศgรeฟcบbถaณ_ฐ]ญZซVจRฆMคIก๙EŸ๕Bœ๑?˜์=”ๅ<=‡าC~ย็Hxต5Gyธ]ฟUอXลjXห๛]าdึiืkึkิjัhอeษcฤaภ`ผ_น^ถ]ดZฒWฐSญOซJชEจAฆ๙=ฃ๕:ก๑8์6™ๅ6“9Šอ?ฝ’YaxD|ฒUฤUรSษัVั^ึeูiูjืiีfาdอaษ_ล^ม]พ\ป[นZทXตTดPฒLฑGฏBฎ=ฌ9ช๚6จ๖4ฅ๑2ข๋1ไ1•ุ8‹ว>‚ธ!=ƒบVม๛KัPวrPฮXี`ูehฺgุeีcา_ฮ]ส[ฦZรYภYพXผWบUนQธMทIถCต?ด:ณ6ฑ3ฐ๛1ญ๗/ช๒.ฆ๋- แ1•ั๛9‹มY6ฤE‡ณPลPฤMหอPำZูbefeูbี_า[ฮXสWวVฤVยUภUฟTพRฝOผKปFปAบ<น8น4ธ1ถ/ต-ฒ๗,ฏ๑+ฉ้.Ÿฺ5”ษณAŽผQย๙IสKศ]Jฯ๛Sื]cedbู^ึ[าWฮTหSศRฦSฤSยSมRมPภMภIภDฟ@ฟ;พ7พ3ฝ0ผ/ป-น,ถ๗+ฑ๏,จโ3œะถCŒต=“มMล๚Pภ๙GหฏJำUฺ^cdb_ู[ึWาSฯQฬPษOวPฦPลQฤPฤOฤLฤIฤEร@ร;ร7ร3ย1ม/ภ-ฟ,ผ+ธ๕,ฐ้2ฃืส?—ย=šวHศHว๛,Dฮ็LืX_฿b฿b_[ูWึSาOฯMฬMสMษNศOวPวPวOศMศJศFศBศ=ศ9ว5ว2ฦ0ล.ฤ-ม,ฝ๙,ถ๎3ฉฯ@ษ?ŸฬAหDษdCัNฺY_เaเ`฿\XูSึOาLฯJอJหKสLสNสOสPหPหOฬLฬIฬEฬ@ฬ;ฬ7ห4ห1ส/ษ-ฦ,ย-บ๑4ฎ฿สCกฬAคะJฤ๓ษ@hAสšCิPZเ_แ`แ]฿YTูOึKำHะGฮHอJฬLฬNอPอQฮRฯQะPัMัHัCั?ะ:ะ6ฯ3ฮ0อ.ส-ฦ.พ๓6ฒโธLกศDจำFฤ๓Gม๑ ?ฬยCึQZเ^โ^แZ฿UPูKึGำEัEฯFฯIฮLฯOะRัTาUำUิSีPีLีGีBิ=ิ9ำ5า1ะ/อ.ษ0ภ๔9ดใ˜KชำCล๒Bฤ๑=อ๛ูCืQYแ\โ[แW฿RLฺGืCิBาCัEัHัLาPำTิVึXืXุVูTูPูKูEุ@ื;ื7ี3ำ0ะ/ห2ภ๓=ถใk6บ้Lญึ@ล๐@ฤ๐$;อ๛ไBุOXโZโYแTเOIฺCุ@ี?ิAำDำIิNีSึWุZฺ[[ZWSMHB=ฺ8ุ4ึ1า0ฬ6ภ๑๏Bทโ8@ธไ?ล๎>ฤํ&9อ๙ๅ@ุM฿UโWใUโQเKE@ู=ื=ึ@ีEึKืQุVฺ[^_฿_เ]เZเUเP฿J฿D>:ฺ6ุ2ิ2ฬ:ภ๏ฦJตGธแ>ร๊>ย่7ห๖<ืIQโSใQใMแGA=ฺ;ุ<ุ@ุFูMฺT[`เcโdใcไ`ไ\ไWใQโKแEเ@฿;7ู3ิ5ส๚?ฟ์"าPถ>ภๅ?ฝแ5ษ๑ห7ีCKแNใMใIโCเ=98ฺ;ฺ@ฺGPXเ_ใdๅh็i่h่d่`็ZๆSๅMไFโAแ<7ฺ5ิ:ศ๗๋Eพ้5Cภ๋@ผ฿Uงม5ล๋ค3ั๚ใ:โ6แ4เ4เ8เ?แIโSๅ^่f๋l๎p๐q๑o๑k๐f๏_ํW๋P้I็Bไ=เ8ฺ;ฮ๛๕Eฤ๏NAว๓Žกท8ฝ฿9ผ*1ฦ๋ใ/ำ๚48฿9แ8โ6ใ3โ1โ2โ6ใ>ไHๆS่^๋g๎n๑r๓s๔r๕n๔h๒a๐Z๎R์J้Cๅ=เ:ืAห๗ถOฟ็ Kร๋=ทุKจล3ภใ›-อ๓.ุ12เ2โ1ใ0ไ/ไ0ไ4ๅ<็E้Q๋\๏f๒n๕r๗t๘s๘o๗j๕c๓[๑S๎K๊Dๅ>฿?ำ๓Gษ๓MBอ๘จทFฌส4ผ7บฺ;.ฦ้๊*ี๙,.฿.โ.ใ-ๅ-ๅ.ๆ2็8้A๋L๎X๑b๔j๗p๙r๚r๚o๙j๘c๖\๓T๏L๋Dๅ@Eฯ๙กTภ่Oฦ๏;ดำHฃฟ2ภเ*ฯ๑(ฺ*,แ,ใ,ๅ,ๆ-่/้4๋<ํF๏Q๒[๕d๘k๛oomi๚c๘\๔T๐L๊EโEึฺMฬ๔-Jฮ๗ “ฉฟTo~–Žsข“ sฆ• Šก–8บื8ธุ-ศๆว&ึ๖(*เ,ใ-ๅ-็-้/๊2์7๎?๐I๓S๖\๙c๛hkjga๙[๕R๏K็H๒Lา๙\8๋Uศ๎SšƒR‘zTฅ)Wช’vXฎ–ญUฒ™ฤLถœม;บข+ฝd!ผ–3ฟ8'ฯ์ื%ฺ๙)฿-โ/ไ/็/้0๋1ํ4๏:๑A๓J๖S๘[๛`ddb^๙W๔OํKแ๙Mึ๛~Xฦ๎Sฮ๔EฆˆCข„ GซmHฐ“เGด—Gธ›HผžDฟก8ยข'ลฃฦขฺฦก‚ ววh%ำํำ(๚/แ3ไ3ๆ3่2๋2ํ4๏7๑<๓C๕K๘R๚X\]\๚W๗Q๐Mๅ๗N†Tฮ๖ Rำ๙>ช†=งƒ ?ฏŒ†<ด‘๗6ธ–5ผš8ฟž<ยก@ลฅ@ศง9สฉ*อฉฮจฯง฿ฯตบีแ,๘9โ:ไ8็7้5์5๎6๐9๒>๔D๖J๘O๙S๚T๙S๕O๏Nๆ้OpUั๗Rึ๚7ฌ…:˜l9ฑŠr3ถ๘,บ”+พ˜.ม3ลก8ศค<สจ?อซ>ฯญ:ัฎ0ิฏ%ีฏืฎุณ๗ฺห.฿์>฿๛?เ=ๅ:่8๋7ํ8๏;๑>๒B๓F๔I๓J๏K้๗LใตO?Yอ๖Tี๙.ฏ„0ฎƒ)ต‰ฬ"บ$ฟ–)ร›-ฦŸ1ษฃ7ฬง=ฯซAาฎBีฑAืณ=ุด7ฺด/ถ(฿ท!แน"ใย1ูฦBำ๓ฝAุ๘๏>;โ9ๆ8่9้;๊>้A่DๅไHโชKRPื๚ MXษ๖#ฏ‚%ช}ด‡=ปŽ  ภ”้"ลš'ษŸ-ฬค5ะฉ>ิฎDืฒHฺตIธGนD฿บ@แป<ไพ;็ม9็ฟ/โท3Iผ่ Cห๊8Bะ๑n@ิ๕›=ุ๘ถ<ฺ๚ร<๛ภ>๛ฎA๛ŠD๛YIู๚'Oั๗Jู๚[บํ+น0ถ‹'ฝ‘4$ม•"ฦšโ#ห *ัง9ึฎFตO฿บTใพUๅมU่รV๋ฦW๎ษS์วKๆภI\ใKตQณฯXจมJมเGศ๊ Hส์ Rรๆ‹‚ถtšศ8บDณ‹.ภ•*(ลš„%ห )าง6ฺฐHแบW่ม`์วc๐หb๐ฬ๕]ํษฑU็ย5Jุณ@ป’Kฒ‹3ฤ™!-ห m,างด1ุฎี<ตูGโปฤMไพ“MโฝHFฺต OๆมJ=rฆ{<ฤ›/ฬข,ฯฆ+ฬค ฒ‹ผ•วภ๘เภ€?๘๘๐๐เเเเภภภภภภภเเเ?๐?๐๘๐เภภภ๐?เ(@€ @้[ฏทƒ่ฎ‚ๆฦwต|฿ศ•๒J —›๚ˆ™๙f~•๗ฉyŽ๑บy†้›~~เPŒuี ||Eฉไš๓šฃ™๚ T€ŸวsŸiœc–๘`๐b‡ๆ๖j~งqwฯ k{ึšcฎฯ‘แฑ– จ•ฅ“0ฃ‘๛=ข๙BขŽ๘AกŽ๗B˜“๙`‰˜ฏz๔n h d_™๚X–๔P‘ํO‰โUิฉYyย W|วดฟ›ฆกŸŸM›ž‹—œฝ“š—์Ž”๓‘๖๚๖๙๖ŠŽ๘}‘๚o•g˜cš`™๛\˜๘T—๔J•๎A’็?Šู๕CƒสIC„อคงจฆจ-˜ง„“ฆัŒค๘†ฃก}žz›x˜w•w‘x๚xŠ๗w‰๕p‰๔gŒ๕`๖\‘๕X“๓P”๐F”์<”ๆ57ˆอ„,“้>ƒฝฅฃ„ป˜ฎ“ฎ†ฎใ…ญ~ฌyชuจsฅqขoŸn›m˜m”llŒ๙m‰๖l†๒g„๐`„๎[†ํV‰์PŒ๋GŽ่<ไ44‰อซCyจ:„ภ’ฑ–ฏณJŠณฬƒต{ตvดsฒpฏnฌmจkฅjขižh›g—f”f๛eŒ๘eˆ๔d…๐a‚์]€้Y€ๆUไO„แD‡9ˆุ5„หส<~น;ฝŠด‹ณ‡ถn‚ธ๋zปuปrปpนoถmณlฐjฌiจgฅfกežd›c—b”a๚`๖`‰๒_…๎^‚๊\~ๅ[|แZ{W{ูM}ำD|ศ๋Exป2Eyฝ€ทตน|{ผ๕tฟqมpภoฟoฝnบlทkดiฐhฌfจeฅdกcžb›`—_”^‘๘\Ž๕[Š๑Z†ํZƒ่Zใ[{]wุ`tา^tษ_qพ‰|^กjjตyนzตxผtuฟ๖pรnลnลoลoรoมnพmปkทjณhฏfซeจdคbกaž_›]˜\•๛Z’๗X๔VŒ๐Uˆ์U…็UโW|Zwึ_sฮeoร๎ikบXOฃkfฐtบjหrฝZoม๏lฦkศmษnสoษoวoฤnมmพkบiถhฒfฎeซcจbค`ข_Ÿ\œZšX—๙V”๖T‘๓RŽ๏P‹๊P‡ๆPƒแPSyิYtห`nฟแckถ?bmผdgฎnบkภlพ4jยgวhหjอmอoอoฬoสoวnฤlภjผiนgตfฑdฎcซaจ`ฅ^ฃ\ YžV›T™๘Q—๕O”๒M‘๎LŽ๊JŠๅJ†เJ‚ฺM|าRvศYpผศ[lฑ [mดfฟgพeยดcศcฬgฯjะmัnะoฮoฬnษlลkยiพgปfทeดcฑbฎaซ_ฉ]ฆZคXขU Rž๛Oœ๘M™๕J—๑H”ํF‘้EŽไDŠ฿D…ุFะMxฤRrธ”VkคToฎdฟ6aยs_ว_อcะgาkำmำnาnะmอlสjวhรgภeผdนcถbณaฑ`ฎ^ฌ\ชYจVฆSคPขM ๚Jž๗Gœ๔Dš๑B˜ํA•้?‘ไ>?ˆืB‚อHzฟ๏LuดGKwธPoง]ย]ม.[ฦ[ฬ_ัdิhีkีlีmำlัkฮiหgวfฤdมcพbปaธ`ต_ณ^ฑ\ฏZญXฌTชQจNงJฅGฃ๚Dข๗A ๔?ž๑=›ํ;˜้:•ใ9:‹ี>„ษD|ปฏItซ GwฐZม]ผXล™WหZั`ิeึhืkืkึkิjาhฯfหdศcลaย`ฟ`ผ_บ^ธ^ถ\ด[ฒXฑVฏRฎOฌKซHชDจAง๚>ฅ๗<ฃ๕9ก๑8Ÿํ6œ่5˜ใ5“7า<„ร๑B}ถBA~ธzคโUลVฤ>Sษ์Uฯ[ิaืeุhูjุjืiิgาeฯcฬaษ`ฦ_ร^ภ]พ]ป\บ[ธZทXตVดSณPฒMฑIฏEฎBญ>ฌ;ช๛9ฉ๘6ง๕5ฅ๑3ขํ2Ÿ่1›โ1•ู6ฬ<„ฝ”a]uB~ฒUยXฝRวขQอUำ]ืbูfฺhฺiูhืfีdาbฯ`ฬ^ษ]ฦ[ฤ[ม[ฟZฝZผYบXนVธTทQถNตJดFณCณ?ฒ<ฑ9ฐ6ฎ๛4ญ๙2ซ๕1ฉ๒/ฆํ.ข็.เ0–ี7Œลึ?„ถ=…นPฦQลพ:พ7ฝ4ฝ2ผ0ป/ป-น,ท๚+ต๖+ฑ๐+ซ่.ฃ5™ห dr‹>’ภPร๘CฮJวtGฮMีVฺ]addcaฺ^ุ\ีYำVะSอRหQษPวQฦQลQฤQรQรPรOรNยKยHยEยAย>ม:ม7ม4ภ2ภ0ฟ/ฟ.พ-ผ,บ๙+ถ๔+ฑํ-ฉแ4žะบDน>–รMล๚Oร๙GสภFาOุX^b฿c฿ca_ฺ\ุYีUำSะQอOหOสNศOวOฦPฦPลPลPลOลMลKลIลFลBล?ล;ล8ฤ5ฤ2ฤ1ร/ย.ย-ภ,พ+ป๘+ถ๑-ฎๅ4ฃีฦC˜ย@›วHวIฦ๛9Dอ๏GิQฺY_฿bเb฿a_]ฺYุVีRำPะNฮMฬLหLษMศNศNศOวPศPศOศNศLศJษGษDศ@ศ=ศ9ศ6ศ4ว2ว0ฦ/ล-ฤ-ย,ฟ๛,บ๔.ฒ่5งุษDœวBŸหNม๐>ฮEศ๛uBฯIืSZ฿_เaเaเ`]ZฺVุSีPำMะLฮKอJหKสLสMสNษOสPสPสPหOหNฬLฬIฬFฬCฬ?ฬ;ห8ห6ห3ส1ส/ษ.ว-ล,ย,พ๗.ถ๋6ซรGŸษ CขอJล๔PพํBสญAาJูT[฿_แ`แ`เ^[WฺSุPีMำKัIฯHอIฬJฬKหLหNหOฬPฬQอQฮQฮPฯNฯLฯIฯEฯBฯ>ฯ:ฮ7ฮ5อ2อ0ฬ/ส.ศ-ล-ภ๙/นํ8ฎตNŸลDงัGล๕Gฤ๔@หีAิLฺU[เ^แ_แ^เ\฿XTฺPุMึJำHัGะFฮGฮHอJอLอNอPฮQฯRะSัSาRาQำNำKำHำDาAา=า9ั6ั4ะ1ฯ0อ.ห.ศ-ร๚0บํ;ฐš&HฉาCฦ๕Dล๕2>ฬํBีLU฿[แ^โ^โ]เZ฿VRNุJึGิEาDัEฯFฯGฯJฯLฯOะQัSาTำUิUีUีSึQึNึKึGีCี?ี<ิ8ำ5ำ3ั1ะ/อ.ส.ฤ๛3ปํ>ฒu5ทๆPซำ@ว๖Aฦ๔K<อ๙BึMU฿Zแ]โ]โ[แW฿SOKูGึDิCำBาCัEะGะJัMัOาRำTิVีWืXืWุVูTูQูMูIุEุAื>ื:ึ7ี4ิ2า0ฯ/ห/ล๚6ป๋๗AณJ?ดเ<ศ๗?ฦ๓_;ฮAืLTเYโ[โ[โXแU฿QLHูDืBี@ิ@ำBาDาGาKำNิQีTึWืYุZูZฺZXVSPLHฺCฺ?ู;ุ8ื5ึ3ิ1ั0อ0ฦ๙9ป๊Gณ!Eด:ษ๖>ฦ๑i9ฮ@ืJRเWโYใYโVแR฿NIEฺAุ?ึ>ี?ิAิDิHิLีPึSืWูYฺ[]]\[XURNIEA=ฺ9ู6ุ4ี1า0อ3ล๗=ป่ฏUญาKณ9ศ๔=ล๏h8ฮ๚>ืHPเUโWใVใSแPเKGBฺ?ุ=ื=ึ>ึ@ึDึIืMุRูVฺZ]_`฿`เ_แ]แZแWแSเOเK฿FB>:7ู4ื2ำ1อ6ฤ๔Bปๆl8ภ๏Qดู9ฦ๏<ฤ์\6อ๘;ึEMเRโTใSใQโMเHD@=ู;ุ;ุ=ื@ุEุJูOฺTY]฿`เbโcใcใbไ`ไ]ใYใTโPโLแGเC฿?;85ุ3ิ3อ;ย๐ๅHบใ,Fปๅ;ร้<ย่F5ห๔๗8ีAI฿NโPใPใMโJแE฿A=:ฺ9ฺ:ู<ู@ูEฺLQW฿]แaโdไfๅgๆg็e็b็_ๆ[ๅVๅQไMใHโDแ@เ<86ุ4ิ6ห๛@มํฉXฒืMนโ<ภๅ=ฟไ+4ศ๐่4ำ<ฺDIแLใLใJใFโBเ>฿:879ฝเ@ป5ล๊ศ1ฯ๘7ุ>CเFโGใEใBโ?แ;เ8฿667;AG฿OแVใ]ๅb็g้k๋m์nํmํkํh์d์_๋Z๊T่O็JๆEไAโ=เ97ุ7ะ@ฦ๒ศNผไKพ็Aบฺ6มๅ‘0ห๒2ี8=฿@แAโ@ใ>โ;โ8แ6เ4เ4฿6฿:เ@เHแOใWๅ_่e๊j์n๎p๏q๐p๐n๐j๐f๏aํ\์V๋Q้K็FๆAใ=เ97ื;อ๚Eฤ๎o8อOผไ6ฟแ8ฝ฿M1ฦ๋๗/ั๙3ุ7:เ<แ;โ:ใ8โ5โ3โ2แ2แ5แ9โ@ใGไOๆX่_๊fํl๏p๑r๒s๓r๓p๓m๒h๑c๐^๎XํR๋M้G็Bไ>แ:9ิAส๖าMม้Jร์<นฺ=ทื3มไษ-อ๓/ึ256เ6โ6โ4ใ2ใ1ใ0ใ1ใ4ใ8ไ>ๅF็N้W๋_ํf๏l๒p๓s๕t๖s๖q๖n๕j๔d๒_๑Y๏SํN๋H่Cๅ>เ;>ัGว๑t*Uพไ>ถึ)ฦ๊6ผr.ว๊,า๘.ู01฿2แ2โ1ใ0ไ/ไ/ไ0ๅ2ๅ6ๆ<็D้L๋Uํ]๏d๒k๔p๖s๗t๘t๘r๘o๗k๖f๔`๓Z๑T๎N์I้Cๅ?เ=ุDอ๘ฬOฤ์Lฦ๏9ธุ;ถี2ภโา+ฮ๑*ื,..เ/โ.ใ.ไ-ๅ-ๅ.ๆ0็4่:้A๊I์Q๎Z๑a๓h๖m๘q๙s๚s๚r๚o๙k๘f๖a๔[๒U๐OํI้Cไ?Bำ๗Jส๔^AาRร๋Cฎอ ะ๔6บฺm-ว้๛)ิ๗)ฺ+,เ-โ-ใ-ไ,ๅ-ๆ-็/่2้7๊=์DํM๐U๒]๕d๗j๙n๚p๛qpn๛k๙f๘a๖[๔U๑OํI่CใBูHฯ๘จTล๋ Pศ๏9ทึ<ณา2ภเท)ฯ๐'ู๛(*฿+แ,ใ,ไ,ๆ,็-่.้0๊4๋9ํ@๎G๑O๓W๕^๘d๚i๛lnnli๛e๙`๗[๔U๑N์H็D฿HิูOห๓.Lฮ๗ฎษ?y8ิŸž‰‹ƒ›“‚Ÿ”››”p“š”ฏ2ฟ7นื7-วๅ%ี๔&(*เ,โ-ไ-ๆ-็-่.๊/๋2์6๎;๏B๑I๓Q๕X๘^๚cgijigc๚_๘Y๕S๐M๋HใIู๐Nะ๘Y9ๅXศ๎[žŠ\™†[ฃ1[ง’p\ซ•ŸZฎ—ธUฒ™ปKต›ช?ธœ‚7บœG5ธš3ฟT(อ้๋$ุ๗'*฿-โ.ไ/ๅ/็/้/๊0๋1ํ4๎8๐=๒C๔J๖Q๘W๚]๛adeec`๛\๘W๔Q๏K็J๘Nิ๚{\ฤ้Uฬ๑H˜€gํภKค‰*Nช›Oฎ“้Nฒ—NตšMธœHปž?พ 1ภ $ม ๕ยŸฦยŸnพ.ลเ^%ั์่%ฺ๗).แ1ใ1ๅ1็1่1๊1์2ํ3๏6๐9๒?๓D๕J๗P๙V๛[^``_\๚X๖S๑N๊Lแ๙Oื๛ŠVฬ๔ Sั๗?ก)€]Dฉ‰JEฏึCด”?ท—?นš@ผCพŸDภขCยค=ลฅ1วฅ!ษฅษคศฃิวขz!ษฮu$ิ์'๘.เ5โ6ไ5ๆ4่4๊4๋3ํ4๎5๐8๑;๓@๕E๖J๘P๙T๛WZZ๛Y๚V๗R๒N์Mโ๔OฺƒUะ๕ Sิ๗9ข”?ฌ‰Q?ฑๅ9ถ“3ธ•2ป˜4พœ7ภŸ:ยก>ลค@วฆ?ษจ<หฉ4อช'ฮชะชะจฯงฺฯทนี฿แ)๕6แ;ใ:ไ9ๆ8่7๊6์6ํ6๏7๐9๒=๓A๕E๖I๘N๙Q๙R๙S๘R๕O๑M๊MโโPฺeWฯ๕Tิ๘6ง‚=ฒ‹;ฎˆ<9ณ฿1ท‘,บ”+ฝ—-ภ›0ยž3ฤก7วค;ษฆ>หฉ?อช>ฮฌ;ะญ6าฎ-ำฎ$ีฎึฎืญืฒ๔ุส)๊;เ?เ>แ=ๅ;็9๊8์7ํ8๏8๐:๑=๒@๔D๔G๕I๕K๔K๑K์Kๆ๗MเฏQฺ๛6eม๋Wั๕2ญƒ4ฌ‚3ณ‰ป*ทŽ%ป’'พ–)มš,ร/ฦ 2ศฃ6สฅ:อจ>ฯซ@ัญ@าฏ@ิฐ=ีฑ8ึฒ3ุฒ,ูณ%ดต฿ถเภ&แื:ฺํ๓Aึ๗๚@?แ=ไ:็9้8๋8์9ํ;๎=๏?๏A๎C์E้Gๅ๓JแนNWSี๘ Neพ๋*ฎ‚,ญ#ณ‡ฎน๙พ“#ม—'ฤ›*วž-ษก1หค6อง;ะซ?าฎBิฐDึฒDุดCูต@ฺถ<ถ8ท4ธ/เบ+ใผ)ๅฝ'ๅฝ๕-ฦkDส๊\Bฯ๏ฌAิ๔เ?ุ๙๙=;เ9โ8ไ8ๅ:ๅ<ๅ>ไAโ๏DเหGŽKฺ๛CQึ๘ =์|กหซ&ธ‹ ฒ…!ธ‹W ผถภ”๓ ฤ™#ศ(หก.อค4ะจ;ำฌAึฐEุณHฺตJธJนHบF฿ปCแผ@โฝ>ๅฟ>็ม?้ย;ๅฝ˜2ณ 8ยHพูEศๆ#Cฬ์NAะ๐y@ำ๔š>ี๖ฏ=ื๘ธ>ู๙ต?ู๙งBฺ๙ŠDู๙bGุ๙6Lิ๖]ล้Uฬ๐Bฏ†%ฝ‘1ท )ฝ‘H%ภ”จ!ฤ˜ํ ษ#อข+ัง6ิฌAุฑHตNนQแผSโพSไฟRๅมQ็รR๊ลTํศTํศN่รฆEเบGโฝXณฮ`ฃฝPบืKรโNรใ[นุˆญzœบญ†l)ฟ”=ท/ฟ”<)ร˜›$ศœ่"อก'าจ4ุฎCตOแปWๅภ]้ฤa์วc๏สf๑อf๓ฯ`๐ฬ๒V้ลL฿ปOไฟ9ผ“@ธ2ม—3,ฦ›(ฬก฿'าง.ุฎ<ตJไผV้ย]์ว^๎ศ\ํศ๑W้ฤถQใฟMDุณJน?พ”Kต7ฤš&1ห l-ะฅฌ-ีชฮ2ูฏุ:ดะ@ทณDธ€Dถ>=ิฏ Lๅฟ%น˜u[Cภ˜4ษ /ฬฃ.ฬค+ฦ~Vชƒ๐ภ€๐เ€๘๐?๐?เภภ€€??€€แภ ๘๘?๐เเ๐ƒ?ภ๐(€ ^ฦ^ฦ^ฦ^ฦช“๔ณ“๒”๔!•’๓U๒}ŒŒ๏Œ‡๊†Žƒ็h’โ7œyeำฐsฺฉš๗ฝš๓›š๚.™๙І™๙ุ}—๘๖w•๗s‘๔r๐t‰์y„ๆ๊€~แถˆz]“uีP€โทpะญš๕qpŸ›๚“œ๛y‡{žržlœhš๛e—๙c“๖b๒b‹ํf†็o€เ๖yzูณ‚vำ:กlย‰tฯฆฬ™™ฅ™๘ ˜›๛Rย€Ÿ๚uกnกj gŸdœb™๛_—๘]”๕Z‘๑X์Yˆๆ`‚฿l{ึtwฯPŠfญxtศถ”๕ม“๔ง”๘—๚I’™๛ง‡œ๑{Ÿrกmกjกh fŸcaš^˜๙[–๖W”๓S‘๏QŽ๊PŠๅU„`}ำgxษ>a|ัspปธ๗ฟ‹๑ฒ“ญ•ช“$ฉ“๛<จ’๛Qง’๚aฆ๚jฆ๙pฅŽ๘rฅ๘qฅ๗mคŽ๗lขŽ๗sž‘๘Ž—”๙ย—๛๐šwœpžlŸiŸhŸfŸdbœ`š๛]™๙Y—๖U–๓P”๐L’์I็H‹โL…ูV~อท\yมZ{ลส’ฐšฉœง›Kคš{ก™ฆŸ˜ส—ไ›–๎™•๕˜”๚—’–‘–๛–๚–Ž๙–Ž๘•Ž๘‘๘Š‘๙“๛x–q˜lšh›fœeœdœb›`š๛^™๚[˜๘W—๕S–๓N•๐H”์D’้Aไ@ŒE„า๖LฦWIสWyปฉ ซ ฆก'ค d Ÿคžู™ž๓•‘œŽœ‹š‰™‡—†–…•…“…‘……Ž๛…๚…Œ๘†Œ๗„Œ๗€๗y๘r‘๚l“๛g•d–c—a˜`˜๛_˜๙\—๘Y—๖U—๔P–๒K–๏F•์A”้=“ๅ:เ;Šึ@ƒษกH}ทD€ภชกฒงคขคPŸฃŸ›ฃ–ฃ๚‘ขŒขˆก… ƒŸž~›}š|˜{–{•{“{‘{{Ž๛{Œ๙|‹๘|Š๖|‰๕y‰๕tŠ๕nŒ๖iŽ๗d๘a‘๘`“๘^”๘\”๗Z”๕W•๔S•๒O•๐I•๎D•๋?”้:”ๅ7’แ5Žู9‡ฬึ@‚ฟ>ƒรฅฅญกกงžจg›งภ–ง๔‘ง‹ง†งƒฆ€ฅ~ค|ฃzกy wžvu›ušt˜t–t”t“t‘st๚t‹๘uŠ๗uˆ๕t‡๔r‡๓n‡๓jˆ๒eŠ๓a‹๔_๔]Ž๔[๔X‘๓U‘๑Q’๐N“๎H“์C”๊>”่9”ไ5“เ2ฺ5‰ฮํ;„ม;9…ฤฌญƒงžซ›ซc˜ชว“ซ๙ฌ‡ซƒซช}ฉ{จyงwฆvฅtฃsขr qŸqp›pšo˜o–o”o’nn๛o๙o‹๘o‰๖o‡๔o†๓m…๑j…๐e…๐a…๏^‡๏\ˆ๏ZŠ๏W‹๏TŒ๎QํM์H๊C‘่>’ๆ9’ใ5’เ2ฺ3Šฯ๙8„ย^7†ลœซŸฉ™ญB–ฎต‘ฎ๗‹ฏ…ฐ€ฐ}ฏ{ฎxญwฌuซsฉrจqฆpฅpฃoขn mŸml›lšk˜k–k”j’jjŽ๛jŒ๙kŠ๗k‰๕k‡๓j…๒h„๐fƒ๏bƒํ_ƒ์\ƒ๋Z„๊W…๊U‡๊R‰้OŠ่J‹็Eๆ?Žไ:แ52Žู2‰ฯ7„ร|2ƒวB‹ฟคฑŠฎ—ฐ”ฐฑ่Šฒ„ณด|ณyณwฒuฑsฐrฎqญpฌoชnจmงmฅlฃkขj jŸii›i™h—h–g”g’ggŽ๚gŒ๘gŠ๖fˆ๕f†๓f…๑eƒ๏c‚ํa๋^€๊[€่Y€็WๆUƒๅR„ๅN†ไIˆใC‰แ=Š฿8‹4‹ุ2‡ฮ7ƒร›K}ฌ?บฏŠŸ’ฑ4ฒถŠด๛„ถท{ทxทvถtถsตrณpฒpฑoฏnญmฌlชkฉkงjฅiฃiขh hžgœg›f™e—e–e”d’d๛dŽ๙dŒ๗dŠ๖cˆ๓c‡๑c…๏bƒํ`์_€๊]่[~ๆY~ไX~ใVแT€เP‚฿KƒD…>†ู8†ี5„ฮ8€ยภ?|ถ=~บฏ•ชณRŠตื…ทน{บxบuบtบrนqธpทpถoดnณmฑlฐlฎkฌjชiฉiงhฅgฃgขf fžeœe›d™d—c•c”b’b๚aŽ๙aŒ๗a‹๕a‰๓`‡๑`…๏`ƒ์_๊^€่]~ๆ\}ไZ|โY{เY{W|U}P~ูI€ึBำ<€อ<}ยไAyธ)?zบŒดŽณ‰ดi†ท้น{ปwผuฝsฝrฝqผpปpบoนnธnทmตlณlฒkฐjฎiฌiชhจgงgฅfฃfกe eždœcšc™b—b•a”a’๛`๚`๘_๖_‹๔^‰๒^‡๐^…๎]ƒ์]‚้\€็\~ๅ\|โ[{เ[y[y\xู[xึXyิRzะK{ฬFzร๚HwนcG{ภIlช„ด…ณ „ถw‚น๐}ปxพuฟsฟqภqฟpฟpพoพoฝnผnบnนmทlตkดkฒjฐiฎhฌhชgจfงfฅeฃdกdŸcžcœbšb™a—`–`”_’๛^‘๙^๗]๕]Œ๓\Š๑\ˆ๏\†ํ[„๋[‚้[€ๆ[~ไ[|แ\z฿\x^wู_uึ`uำ`uฯ]uหXuลUsปถVpฏVqดถ‚ดธz~บ๓yฝuฟsภqมpยpยoมoมoมoภoฟnพnผmปmนlทkตjดjฒiฐhฎgฌgชfจeฆeคdฃdกcŸbžbœa›`™`˜_–^”๛]“๚]‘๘\๖[Ž๔[Œ๓ZŠ๑Y‰๏Y‡์Y…๊Yƒ่YๅYใZ}เ[{\x^vื`tิbsะdrอerศdpภ๖emธgŠG…iiฒ~ถ€ณ}นr{ผ๒vฟsมpยoรoฤoฤoฤoฤoรoรoยoมoฟnพmฝmปlนlทkตjณiฑhฏgฎgฌfชfจeฆdคcฃcกbŸaž`œ`›_™^˜]—\•๛\“๙[’๗Z‘๖Y๔X๒W‹๐WŠ๎Vˆ์V†๊V„็V‚ๅV€โW~เX|Yyฺ[wึ]tา`rฮdqสgoลjmฝ฿liถBemหoiญyธzณyนbwฝํsภpยnฤnลnฦnฦnวnวoฦoลoลoฤoรoมnภnพmฝmปkนkทjตiณiฑgฏgญfซeฉeจdฆcคbฃbกa `ž__›^š]™\—[–๚Z”๙Y“๗X‘๕W๓VŽ๒U๐U‹๎T‰๋T‡้S…็SƒไSแT฿U}VzูXwีZuั]rอapศfnยikปอkhณ/ilปm`งuทvษvบJtฝใqมnรlลlฦlศlศmษnศnษoศoวoวoฦoฤoรoมnภmพmผlบkนjทiตhณhฑgฏfญeซeฉdจcฆbคbฃaก` _ž^]œ\š[™Z˜๛Y–๚X•๘W”๖V’๕U‘๓T๑SŽ๏RŒํQŠ๋Qˆ้Q‡ๆQ…ไQƒแQR~S{ุTyิVvะZsฬ^pฦcnภgjธนhgฐhhณ~ฐqพsผ.qพะnมlฤjฦjศjษkสlสmหnหoหoสoษpศpวoฦoฤoรnมmฟlพlผkบjธiถhดgฒfฐfฎeญdซdฉcจbฆaฅ`ฃ`ข_ก^Ÿ]ž\œZ›YšX™๛W—๙V–๘U•๖T“๔S’๒R‘๑Q๏P์OŒ๊OŠ่NˆๆN†ใN„แO‚O€O}ืQzำSwฯVtส[qล`nฝckถžehฎdiฑoผpผoพฐlมiลhวhษhสjหkฬlอmอnฬoฬoฬpหpสoศoวoฦnฤnยmมlฟkฝjปiนiทhตgณfฒfฐeฎdฌcซcฉbจaฆ`ฅ_ค_ข]ก\ [žZYœW›Vš๚U™๙T—๗S–๕Q•๔P“๒O’๐N๎N์M๊L‹่LŠๅKˆใK†เL„LฺM~ึN|าPxฮSuษXrร]nป๘`kณydgฅaiฎmปoทlพ‚jม๛gลfศfสfหhอiอkฮlฮmฯnฮoอoอpฬoหoสoศnวnลmรlยkภkพjผiบhธgถgตfณeฑeฐdฎcฌbซbฉaจ`ง_ฅ^ค]ฃ\ข[ ZŸXžWUœ๛T›๚Sš๘Q˜๗P—๕O–๓N•๒M“๐L’๎K์J๊J็I‹ๅH‰โH‡เH…I‚ูI€ึK}ัMzอPwวUsมYoธ่[lฐJZpน\hซoผbยiพMgม๋eลdศcสdฬfฮhฯjฯkะlะmะnะoฯoฮoอoฬoหnษnศmฦlฤlยkภjฟiฝhปgนgธfถeดeฒdฑcฏcฎbฌaซaช`จ_ง^ฆ]ฅ[ฃZขYกW VŸUžS๛Rœ๚P›๘Oš๖N˜๕M—๓K–๑J•๏I”ํH’๋G้G็FๅE‹โE‰฿E‡F„ูFีHะJ{หNxลRtพVoตลWmฎWmฐgพhฝ eมวcลaศbหbอdฮeะhัjัkัlัnัnัoะoฯoฮoอnหnสmศlวkลkรjมiฟhพgผgบfธeทeตdณcฒcฑbฏbฎaฌ`ซ_ช^ฉ]จ\ง[ฅZคXคVฃUกT RŸQž๛O๙Nœ๘L›๖Kš๕J™๓I˜๑G–๏F•ํF”๋E’้D็CŽไCŒโB‹฿BˆB†ุCƒิE€ฯG}สKyฤPtปRpฒ‡VmฅToญeฟgฝdภ‹aฤ_ศ_ห`อbฯdะfัhาjำkำmำmาnาnัnะnฯnฮmฬmหlษkวjลjฤiยhภgพfฝfปeนdธdถcตcณbฒbฑaฏ`ฎ`ญ_ฌ^ซ]ช\จZงYฆWฅVคTฃSขQกP N ๚MŸ๙Kž๘J๖Hœ๔Gš๓F™๑E˜๏D—ํC•๋B”้A’็A‘ไ@โ@@Š@‡ืA„ำBฮE~ศIzมMtทๆQqฎ>Osฑafจjฟ๚_มbฟG`ร๊^ว]ส^อ`ฯbัdาfำhิjิkิlิmำnาnาnัmะmฮlอlหkสjศiฦhฤgยfมfฟeพeผdบcนcทbถbดbณaฒ`ฑ`ฐ_ฎ^ญ]ฌ\ซ[ชZฉXฉWงUฆSฆRฅPคOฃMขKก๚J ๙HŸ๗Gž๖E๔Dœ๓C›๑Bš๏A™ํ@—๋?–้?”ๆ>’ไ>‘แ=Ž=Œฺ>‰ึ>†า@ƒอCฦGzฝKuณงOpช Mrฎaภaพ_ยน\ฦ[ส\อ^ฯ`ัbาeิgิiีjีlีlีmิmำmาmัlะlฯkอjฬiสiศhวgลfรeมeภdพcฝcปbบbธaทaถaด`ณ`ฒ_ฑ^ฐ]ฏ\ฎ[ญZฌYซWชVชTจRจQงOฆNฅLคJฃHข๚Gข๙Eก๗D ๖CŸ๔Až๓@๑?œ๏>šํ>™๋=—้<–ๆ;”ใ;’แ;;ฺ;Šี<‡ะ>„สB€รFyน๐IuฑLHwดNoง`ฟ"]มl[ล๙YษYฬ[ฯ^ั`ำcิeีgึiึjึkึlีlีmิlำlาkัkฯjอiฬhสgศfวeลdฤdยcภbฟbฝbผaบaน`ธ`ท`ต_ด_ณ^ฒ]ฑ\ฑ[ฐZฏYฎXญVฌUซSซRชPฉNจLงKงIฆGฅEค๚Dฃ๙Cข๗Aก๖@ ๔?Ÿ๓>Ÿ๑=๏<œํ;›๋:™้9˜ๆ9–ใ8”เ8‘8ู9Œิ:ˆฯ=…ศAฟEyตซJtฉ Hvฎ\ม]ภ%ZฤาWศWฬYฯ[ั^ำaิcีfึhืiืjืkึkึlีlิkำkาjัiฯiฮhฬgสfษeวdลcฤbยbมaฟaพ`ผ`ป`บ_น_ธ_ท^ถ^ต]ด\ณ\ฒ[ฑYฐXฐWฏUฎTญRญQฌOซMชKชIฉGจFงDงCฆ๚Aฅ๙@ค๗>ฃ๖=ข๔<ก๓; ๑:Ÿ๏9žํ8๋8›้7™ๆ6˜ใ7•เ6“6ุ7ำ9‰อ<„ลA~บํEyฑBDzณ[ม_พ๚Yร„VฦUหVฮYั\ำ_ิaึdืfืhุiุjุkืkึkึkีjิjาiัhะgฮfฬeหdษcศbฦaฤaร`ม`ภ`พ_ฝ_ผ_ป^บ^น^ธ]ท]ถ\ต[ด[ดZณYฒWฑVฑTฐSฏQฎOฎNญLฌJฌHซFชDชCฉAจ@จ๚>ง๙=ฆ๘;ฅ๖:ค๕9ฃ๓8ข๑7ก๏7 ํ6ž๋5่4›ๆ4™ใ4—฿4”4‘ึ6ั8‰ส=ƒภB~ถ–KxฃE{ฎYยYม1Vล฿SษTอVะYา\ิ_ึbืdุfุhูiุjุjุjืjึjีiิhำgัfะeฮdอcหbษbศaฦ`ล`ร_ย_ภ^ฟ^พ^ฝ]ผ]บ]บ]น\ธ\ท[ถZตZตYดWดVณUฒSฒRฑPฐNฐMฏJฎIฎGญEญCฌAซ@ซ>ช=ฉ๚;ฉ๙:จ๘9ง๖8ฆ๕7ฅ๓6ค๑5ฃ๏4กํ3 ๋3ž่2œๅ2›โ1˜2•ฺ3’ี5Žฮ9‰ฦ>‚ปC}ณ&A~ตYย\ฟ๙Vฤ‘SศRฬSฯVาZิ]ึ`ืbุeูfูhูiูiูjุjืiึiีhิgำfัeะdฮcอbหaส`ศ_ว_ล^ฤ]ย]ม]ภ]ฟ\ฝ\ผ\ป\บ[บ[น[ธZทYทXถWถVตUดTดRณPณOฒMฒKฑIฑGฐEฐCฏBฎ@ฎ>ญ=ฌ;ฌ:ซ๛8ซ๚8ช๘6ฉ๗5จ๕4ง๓4ฆ๑3ฅ๏2ฃํ1ข๋1 ่0žๅ0œแ0™1–ุ2’ำ5ห:‡ภ๛@ถh;‡ผEwฐVรWย6SฦใPสQฮSัWิ[ึ^ืaุcูeฺgฺhฺiูiูiุiุhึgีgิeำeาdะbฯaอ`ห_ส^ศ^ว]ล\ฤ\ร\ย[ภ[ฟ[พ[ฝ[ผ[ปZปZบYนYนXธWธVทUทTถRถQตOดNดLดJณHณFฒDฒBฑ@ฑ?ฐ=ฐ;ฏ:ฏ8ฎ7ญ๛6ฌ๚5ซ๘4ซ๗3ช๕2ฉ๔1จ๒1ฆ๐0ฅํ/ฃ๋/ข่/Ÿไ/เ/š0–ึ2’ะ7Œฦ<…บฎD~ญ AณWม[ผ๙Sฤ’PษOอQะTำXี\ื_ุaูdฺeฺgฺhฺhฺhูhูhุgืfึeิdำcาbะaฮ`อ_ห^ส]ษ\ว[ฦ[ฤZรZยZมZภZฟZพZฝYผYผYปXปXบWนVนUธTธSทQทPทNถMถKถIตGดEดCดAณ?ณ>ณ<ฒ:ฒ9ฑ7ฑ6ฐ5ฏ๛4ฎ๚3ฎ๙2ญ๗1ฌ๖0ซ๔0ช๒/จ๐.งํ.ฅ๊.ฃ็- ใ.ž฿.šฺ0–ิ4‘ห9‰ฟใ@„ต*>…ธSรTย2QวแNหOฯQาUีYื]ุ_ูbฺdegghฺhฺgูgุfืeึdิcำbา`ะ_ฮ^อ]ฬ\ส[ษ[วZฦYลYฤYรYมXมXภXฟXพXฝXฝWผWผVปVปUบTบSนRนPธOธMธKธJทHทFทDถBต@ถ>ต<ด;ด9ด8ณ6ณ5ฒ4ฒ3ฑ2ฐ๛1ฐ๙0ฏ๘0ฎ๖/ญ๔.ซ๒.ช๐-จํ-ฆ๊,คๆ,กโ-ž.šุ1•ะ6Žฤ๙<ˆบ`:ˆผT›นVม๚aธ๐Rฤ‰NษMฮOัRิVึZุ]ู`ฺbdefgggฺfูfุeืdึcิaำ`า_ะ^ฯ\อ[ฬZสZษYศXฦXลXฤWรWยWมWภWภWฟWพWพVฝVฝUผUผTปSปRปQบOบNบLบKนIนFธDธCธAท?ธ=ท;ถ:ท8ถ7ถ5ต4ต3ด2ด1ณ0ณ๛0ฒ๙/ฑ๘.ฐ๖.ฎ๔-ญ๒-ซ๏,ฉ์,ง้+ฅๅ,ขแ-ž/šิ3“ษ:Œพ•V‚คC‰ทRฤSร๛'OฦูLหLฯOำSีWุ[ู^ฺacdfffffฺeูeุdืbึaิ`ำ_า]ะ\ฯ[อZฬYหXษWศWวVฦVลVฤVรVยVมVภVภVฟVฟUพUพTพTฝSฝRฝQผPผNปMปKปJปHบFปDบBน@น>บ<น:ธ9น7ธ6ธ5ท4ท3ท1ถ1ถ0ต/ด๛.ด๚.ณ๘-ฑ๖,ฐ๔,ฏ๒,ญ๏+ซ์+จ่+ฅไ,ข.ุ2—ฮ8ยฤ@Šธ>ŒปWร๙,ิPลuLษJฮMัPิTืXู\ฺ_acdefffeฺdูcุbืaึ`ิ^ำ]า\ะ[ฯYอXฬWหVษVศUวUฦUลUฤTรUรUยTมTมTภTภTฟTฟSฟSพRพQพPพOฝNฝLฝJฝIผGฝEผCผAป@ผ>ป<ป:บ8ป7บ6บ4น3น2น1ธ0ธ/ท/ท.ถ-ต๚-ด๘,ณ๖,ฒ๔+ฐ๑+ฎ๎+ซ๋+จๆ+ฅแ-ก0›า7”ฦๅ>Žฝ(<ภQฤ๚Sร๙MวศIหJะMำQึUุYฺ]`bceeeeedฺcูbุaื`ึ^ิ]ำ\าZะYฯXอWฬVหUสTษTศTวSฦSลSฤSรTรTยTยTมSมSภSภSภRภQฟPฟOฟNฟMฟKพJพHพFพDพBพAพ?พ=ฝ;ฝ9ผ8ฝ6ผ5ผ4ป2ผ2ป1บ0บ/บ/น.ธ-ธ-ท๚,ถ๘,ด๖+ณ๔+ฑ๑+ฎํ+ฌ้+จไ,ค/Ÿึ5˜ส๒=‘มI;“รTง๑MหOล๛WJษ๖HฮJาNีRืVูZ^`bcdeeedcbฺaุ`ื^ึ]ิ\ำZาYะWฯVฮUฬTหSสSษRศRวRฦRลRลRฤRรSรSยRยRยRมRมRมQมQมPภOภNภLภKภIภGภFภDฟBฟ@ฟ>ฟ=ฟ;ฟ9พ8พ6พ5พ4ฝ2พ1ฝ0ฝ0ฝ/ผ.ป.ป-บ-น,ธ๚,ท๘+ต๖+ด๓+ฑ๏+ฏ๋+ซ็,งแ.ขฺ3›ฮ;”รh9–ลRร๘Vภ๕ LฦจGหGะKำOึSุXฺ[^`bdddddcbฺaฺ`ุ^ื]ึ[ีZำYาWัVฯUฮTฬSหRสRษQศQวQวQฦQลQลQฤRฤQรRรRรQยQยQยPยPยOยNยMยLมKยIยHมFมDมBภAภ>ภ=ภ;ภ9ภ8ภ6ภ5ภ4ภ2ฟ1ฟ0ฟ0พ/พ.พ.ฝ-ผ-ป,บ,น๚+ธ๗+ถ๕+ด๒+ฑ๎+ฎ้,ชไ.ฅ3žั:—ล€6žฯ?ŒถNล๛Oฤ๚2IศๅEอGัKีPืTูY\_abcdddcbaฺ`ฺ_ุ]ื\ึZีYำWาVัTฯSฮRอQฬQหPสPษPศPวPวPฦPลQลQลQฤQฤQฤQฤQรPรPรPรOรNรMรLรKรIรGรFยDยBรAร?ร=ย;ย9ม8ม6ม5ม4ย2ม1ม1ภ0ภ/ภ.ฟ.ฟ-พ-พ,ฝ,ป๛+บ๙+ธ๗+ถ๔+ด๐+ฑ๋,ญๆ.จ฿2กิ:šศ“nx‘D”พUม๕7ะLฦ๛wFสEฯHำLึQุUฺZ]_ab฿c฿dccba`ฺ_ฺ]ุ\ืZึYิWำUาTัSฯRฮQอPฬPหOสOษOศOศOวOวPฦPฦPลPลPลPลPลPลPลPลOลOลNฤMฤLฤKฤIฤHลFลEลCฤAฤ?ฤ>ฤ<ร:ร8ร7ร5ฤ4ร3ร2ย1ร0ย/ย/ม.ม-ภ-ภ-ฟ,พ,ผ๛+ป๙+น๕+ถ๒+ณํ,ฐ่.ซแ2คื:ห O’ธD˜รNฤ๘Qร๗IวฝDฬDัHิMืRูVZ]`฿a฿b฿c฿c฿cba`_ฺ^ฺ\ุZืYึWิUำTาSัRฯPฮOอOฬNหNสNษNษNศNศNวOวOวOฦOฦPฦPฦPฦPฦPฦPฦOฦOฦNฦMฦLฦKฦJฦHฦGฦEฦCฦAฦ@ฦ>ล<ล:ฦ9ฦ7ฦ6ล4ล3ฤ2ฤ1ล0ฤ0ร/ร.ร.ย-ย-ภ,ภ,พ,ฝ๚+ป๗+น๔+ถ๏,ฒ๊.ญใ2ฆู:ŸอฉK˜พ C›ลJฦ๚Lล๙:Fษ์BฮDาIึNุSW[^฿`฿a฿b฿c฿c฿bb`_^ฺ\ฺ[ุYืWึUีTำRาQัPฯOฮNอNฬMฬMหMสMษMษMศMศNศNวOวOวOวOวPวPวPวOวOวOวNวMวMวKวJศIศGศFศDวBวAว?ศ=ศ;ว:ว8ว7ว5ว4ฦ3ว2ฦ1ฦ0ล/ล/ล.ฤ-ร-ร-ย,ภ,ฟ,ฝ๙,ป๕,ธ๑,ต์.ฏๅ2จ;กฯญK™ม DวQย๕>อJฦ๚xDสBะEิJืOูTX[^฿`฿aเbเb฿b฿aa`^][ฺZุXืVึTีRำQาPัOะNฮMอLอLฬLหLสLสLษLษMษMศNศNศNศOศOศOศPศPศPศOษOษOษNษMษLษKสJสHสGสEสCสAส@ษ>ษ<ษ:ษ9ศ7ศ6ษ5ษ3ศ2ศ1ศ1ว0ว/ฦ/ฦ.ล-ฤ-ร-ย,ม,ฟ๚,ผ๗,บ๓-ถํ.ฑๆ2ช<ฃัซM›ร EžษLฤ๖Pย๓ Gว๛ดAฬBัFีKุPฺUY\฿^เ`เaเbเbเa฿a฿`_]\ZูXุVืTึSีQำPาNัMะLฯLฮKอKฬKหKหKสKสLสLษMษMษNษNษOษOษOษPษPสPสPสOสOสOหNหMหLหKหIหHหFหEหCหAห?ห=ส<ห:ห8ห7ส6ส4ส3ษ2ษ1ษ0ษ0ศ/ว.ว-ฦ-ล-ฤ,ย,ม,พ๘,ผ๔-ธ๎.ณ็3ฌ<ฅำฃQœมGกสJล๗Kร๕)Dศใ@ฮBาGึLูQUY\฿_เ`เaเaเaเa฿`฿_^\ZXูVุUืSึQีPำNาMัLะKฯKฮJอJฬJฬJหJหKหKสLสLสMสNสNสOสOสPหPหPหPหPหPฬPฬOฬOฬNฬMฬLฬJอIอGอFอDฬBฬ@ฬ?อ=ฬ;ฬ:ฬ8ฬ7ฬ5ฬ4ห3ห2ห1ส0ส/ษ/ษ.ศ-ว-ฦ-ฤ,ย,ภ๙-ฝ๕-บ๐/ต่4ฎ>ฆำ˜^•ณHกษGฦ๘Iล๖YBส๗?ฯBำGืMฺRVZ฿\เ^เ`แ`แaเ`เ`เ_฿^\[YWฺUุSืQึPีNำMาLัKะJฯIฮIอIอIฬIฬJหJหKหKหLหMหMหNหOหOหPฬPฬPฬQฬQอQอQอPฮPฮOฮNฯMฯLฮJฮIฮGฮEฮDฯBฯ@ฮ>ฮ<ฮ;อ9อ8อ6อ5อ4ฬ2ฬ2ฬ1ห0ห/ส.ษ.ศ-ว-ล-ร-ม๚-พ๖-ป๐/ถ้5ฎ฿>จิˆ ฑ่LฅฬLฤ๐Fว๘Š@ห?ัCีHุNฺRVZ฿]เ^แ_แ`แ`แ`เ_เ^฿][YWUฺTุRืPึNีMิKาJาIะIะHฯHฮHอHอHอIฬJฬJฬKหLหLฬMฬNฬOฬPอPอQอQอQฮRฮQฮQฯQฯQฯPะOะNะMัLัJัIัGัEะCะAะ?ะ>ฯ<ะ:ฯ9ฯ7ฯ6ฯ5ฮ3ฮ2อ1อ0ฬ0ฬ/ห.ส.ศ-ว-ล-ย๛-ภ๗.ผ๑0ท๊7ฏ฿Aฉีs?ฉึ?ฮ์Jฤ๓Nย๐ Dว๙ถ>ฬ?ัCีIุNSV฿Zเ]แ^แ_แ`แ`แ_เ^เ]฿\ZXVTฺRุPืOึMีKิJำIาHัGะGฯGฮGฮGอGอHอIอJอKฬKฬLอNอNอOอPฮQฮQฮRฯRฯRะSะRัRัRัQัPาPาNาMาLาJาHาFาDาCาAา?ั=ั;ั:ั8ะ7ะ5ะ4ฯ3ฯ2ฮ1ฮ0อ/ฬ/ห.ส.ศ-ฦ-ฤ-ม๗.ฝ๒1ธ๊9ฐ฿๘Cชี[AซืGฤ๓Iร๑Bว๙=อ?าCึIูOSW฿Zเ]แ^แ_โ_แ_แ^แ]เ\฿[YWUSฺQูOืMึKีJิIำGาFัFะFฯFฯFฮFฮGฮHอHอIอJอKอMฮNฮOฮPฯPฯQฯRะSัSัSัTาSาSำSำRำRำQำPิNำMำKำJิHำFำDำBำ@ำ?ำ=า;า9า8า6ั5ั3ะ2ะ1ฯ0ฮ0อ/ฬ.ห.ษ.ว-ล-ย๘/พ๒2ธ้;ฑ๎Fชิ=CฌืFล๔Hฤ๒;@ศ๚๎<ฮ?ำDืJฺOSW฿Zเ\แ^โ^โ_โ^แ^แ\เ[฿Y฿WUSQฺOูMุLึJีHิGำFาEัEัEะEฯEฯFฯFฮGฮHฮIฮJฮKฯMฯNฯOะPะQะRัSัSาTาTำUำUิUิTิTีSีRีQีPีNีMีKีIีGีEีCิAิ@ิ>ิ<ำ:ำ8ำ7ำ6า4า3ั2ะ1ะ0ฯ/อ/ฬ.ส.ศ.ฦ.ย๘/พ๒3น้=ฑ฿Hฌี EฎืDล๔Fฤ๒\>ษ๚๘<ฯ?ำDืJฺOSWเZแ\แ]โ^โ^โ^แ]แ\เZ฿X฿VTRPฺNูLุJืHีGิFิEำDาDัDะDะDะEฯFฯGฯHฯIฯJฯKฯMะNะOัQัRาSาTำTำUิUิVีVีVีUึUึTึSืRืQืPืNืLึKึIึFึEึCึAี?ี=ี;ี9ิ8ิ7ิ5ำ4า2า1ั0ะ0ฯ/อ.ห.ษ.ฦ.ร๘0ฟ๑5น่@ฑพMซาHฎึEฟ๔Aฦ๔Cฤ๒w=ส๛;ะ?ิDืJฺOSWเZแ[โ]โ]โ]โ]โ\แ[เY฿WUSQOฺLูJุIืGึEีDิCำCาCาCัCัDะDะEะFะHะIะJะLัMัNัPาQาRำTำUิUิVีWึWึWืWืWืVุVุUุTุRุQุOุNุLุJุHืFืDืBื@ึ>ึ<ึ:ึ9ี7ี6ิ4ิ3ำ2า1ั0ะ/ฮ/ฬ.ส.ว/ร๘1ฟ๑8ธ็Bฒ•mกฦNญืLภ้Bล๒Ž<ห๛;ั?ีEุJOS฿VเYแ[โ\โ\โ\โ\โ[แZเXเV฿SQOMฺKูIุGืEึDีCิBำBำBาBาBัCัDัEัFัHัIัKัLาNาOาQำRำSิTีUีVึWืXืXืXุXุXูXูWูVูUฺTฺRฺQูOูMูKูIูGูEุCุAุ?ุ=ื;ื9ึ8ึ6ี5ี4ิ2ำ1า0ั0ฯ/อ/ห/ศ0ฤ๘2ฟ๐:ธๅFฑhCตH‹็Kม๋UผโAล๒ก;ห๛:ั?ีDุJOS฿VเYแZโ[โ\โ[โ[โZแXแW฿U฿RPNLฺJูHุFืDึCีBีAิAำAำAาBาBาCาEัFาHาJาKาMำNำPิQิSีTึUึVืWืXุYุYูZูZฺZฺYXWVUSRPNLฺJฺHฺFฺDูBู@ู>ุ<ุ:ื9ื7ึ6ึ4ี3ิ2ำ1า0ะ0ฮ/ห/ศ0ฤ๗4พ๏>ทไ์Iฑฺ8FณGย๋Kฟๆ @ฦ๒ฏ:ฬ๛:ั>ึDูINR฿UเXโZโ[โ[โZโZโYแWแU฿S฿QOMJHฺFูDุCืAึ@ี@ิ@ิ@ำ@ำAำBาCาEาFำHำJำLิNิOิQีSึTึUืWืXุYูZูZฺ[ฺ[[[ZZYWVTSQOMKIGECฺAฺ?ู=ู;ู9ุ8ื6ื5ึ3ี2ิ1า0ั0ฮ0ห0ศ1ฤ๖6พ๎BทโสMฒูJณFย๊Iฟ็ ?ฦ๑ท9ฬ๚9า>ึCูHMQ฿UแWโYโZใZโZโYโXแVแTเR฿PNKIGฺEูCุAื@ึ?ี?ี?ิ?ิ@ิ@ำBิCิEำFิHิJิLีNีPึRืTืUุWุXูYฺZฺ[\\\\\[[ZYWVTRPOLJHECA@>ฺ<ฺ:ู8ุ7ุ5ื4ึ3ี2ำ1ั0ฯ0ฬ0ศ3ร๕:ฝ๋Eถแ“nขฦPฑฺEม้Iฟๆ>ล๐ผ8ฬ๙8า=ึBูHMPเSแVโXใYใYใYใXโVแUเSเQ฿OLJHEฺCูAุ@ุ?ื>ึ>ี>ี>ี?ี@ิAิCิDิFีIีKีMึOึQืSุUุWูXฺZ[\]]^^]]\฿\฿[฿Z฿X฿W฿U฿SQOMKIFDB@><:ฺ9ู7ุ6ุ4ื3ี2ิ1า1ฯ0ฬ1ศ๛4ร๓=ผ้๗Hถ฿WEธแห%zDม้Hฟๅ>ล๏ป8ห๘8ั<ึAูFKOเRแUโVใWใXใWใVโUโSแRเP฿MKIFDฺBู@ู?ุ>ื=ื=ึ=ึ>ี>ี@ีAีCีEีGึIึLืNืPุRุTูVฺXZ[]^^__฿_฿_เ^เ^เ]เ\เ[เYเXเVเTเR฿P฿N฿L฿IGECA?=;9ฺ8ู6ุ5ื4ึ2ิ2า1ฯ1ฬ2ว๚6ย๑BปๆฺMถ"Jท฿Eม่Iฟไ =ล๎ต7ห๗7ั:ี@ูEJNเQแTโUใVใVใVใUใTโRแPเN฿LJGEBAฺ?ู>ุ=ุ<ื<ื<ึ=ึ>ึ?ึAึCึEืHืJืMุOุQูSฺVXZ\]^_฿`฿aเaเaแ`แ`แ_แ^แ]แ\แZแXแVแUแRแPเOเLเJ฿G฿E฿CA?=;:8ฺ7ู5ุ4ึ3ี2า1ฯ2ห4ว๘:ภ๏Eบไ [ฑีPถDภๆJฝแ =ฤ์ซ7ส๕6ะ9ี>ูCHMเPแRโTใUใUใTใTใRโPแOแMเJ฿HFCA?ฺ>ฺ<ู<ุ;ุ;ื<ื<ื>ื?ืAืCืFุHุKูNูPฺRUWZ\]฿_฿`เaเbแbโcโcโbโaใaใ`ใ^ใ]ใ[ใYโWโUโSโQแOแMแKเHเFเD฿B฿@><:87ฺ6ุ4ื3ี2า2ฯ2ห6ฦ๖>ฟ๋๗IนโZEปๆ[ฎัEภใSบุ=ร๊›6ษ๓5ะ8ิ=ุBFJ฿NแPโRใSใSใSใRใQโOโMแKเI฿FDB@>=ฺ;ู:ู:ุ:ุ;ุ<ุ>ุ?ุAุCุFูIูLฺOQTVY[฿]฿_เaแbโcโcใdใdไdไdไcๅbๅaไ`ไ^ไ\ไZไXใVใTใQใOโMโKแIแFแDเB฿@฿><:97ฺ6ู5ื4ี3า2ฯ4ส8ล๔Cฝ่ีNธ฿KนโDผ1ห๗=ม็†6ศ๑4ฯ๚7ิ;ุ@ฺDI฿LเNโPใQใRใQใPใOใMโLแJเGเE฿CA?=;ฺ:ฺ9ู9ู:ู:ู;ุ=ุ?ูAูDฺFฺJMPRUX[฿]เ_แaโbใdใeไeๅfๅfๆfๅeๆeๆdๆbๆaๆ_ๆ^ๅ[ๅYๅWไTไRไPใNใKโIโGแDแBเAเ?฿=;986ู5ุ4ี3า4ฮ5ษ๚<ร๐Gผๆ‘gฎะQธ฿uxต<ย็>ภๅk7ฦ๎3อ๘5ำ9ื>ฺBFIเLแNโOใPใOใOใMใLโJโHแFเC฿A?=;:9ฺ9ฺ9ฺ9ู:ฺ;ฺ=ู?ฺAฺDGJNPSW฿Zเ\แ_โaใbไdไfๅgๆgๆh็h็h่g็f็e็d็b็`็_็]ๆZๆXๅUๅSๅQไNไLใJใGโEโCแAเ?เ=฿;987ฺ5ุ4ี4า5อ8ศ๗Aม์๐KปใEIผๅ=ภๅ?ฟใJ7ล๋๓2ฬ๕3ั7ึ;ู@CG฿IแLโMใMใMใMใLใJโHโFแDเBเ@฿><:988889ฺ;ฺ=ฺ@BEHKOQ฿UเXเ[แ^โ`ไbไdๅfๆh็i่i่j้j้j้i้h้g้e้d้b่`่^่\็Y็VๆTๆQๅOๅMไJไGใEโCโAแ?เ=฿;:87ฺ5ื4ี4ั6ฬ;ฦ๔Fฟ้บRน฿Nผใ>พโ@ฝเ'8ร่ไ2ส๒2ะ๚5ิ9ุ=AD฿GเIโJโKใKใJใIใHใFโDโBแ@เ>฿<฿:9877789;=?BEHLO฿SเVแYโ\ใ_ไbๅdๆf็h่i้j้k๊l๊l๋k๊k๋j๋h๊g๊e๊c๊a้_้]้Z่W็U็RๆOๆMๅKๅHไEใCใAโ?แ=เ;฿:87ฺ6ื5ิ5ะ8ห๚@ฤ๐๚Jพๆh>ยํUผใ@ผ฿Cบ9มๅย2ศ๏0ฮ๗3ำ6ื:ฺ>ADเFแGโHใHใHใGใFใDโBโAแ?แ=เ;฿9฿8766679:=?BEI฿M฿PเSแWใ[ใ^ๅ`ๆc็e่h้j้k๊l๋m๋m์m์m์l์k์j์h์f์d๋b๋`๊^๊[้X้U่S่P็NๆKๅHๅFไCใAโ?แ=เ;฿:87ฺ6ื5ำ6ฯ:ษ๗Eย์ิPฝโ!MพๅEธูgฃน;พแ‘3ล๋0ฬ๕1ั๛4ี8ู;>A฿CเDแEโEใEใDใDใBโAโ?โ=แ;แ9เ8฿7฿6฿55568:แ<฿:87ู6ึ6ำ8อ?ฦ๓Iฟ้„|ฃมSบโ“}€;พแ=ผ[5ย็๘0ส๑0ะ๘2ิ5ื8ฺ;>@เAแBโBโBใBใAใ@ใ?โ=โ;แ:แ8แ7เ6เ5฿4฿4฿5฿6฿7฿9฿<฿?฿BเFเJแNโQใUไYๅ]ๆ`่c้e๊h๋j์lํn๎o๎p๏p๏p๐p๏o๐n๐l๏k๏i๏g๎dํbํ`ํ]์Z๋W๊T๊R้O่L็J็GๆEๅBใ@โ>แ<฿:87ู7ึ7ั;ห๚Dฤ๏โNพๅ1Lภ่=ผ?ป%7ภใ0ศํ/ฮ๕0า๛2ึ5ู8;=฿?เ?แ@โ@โ?ใ?ใ>ใ=โ;โ:โ8โ7แ6แ4แ4เ3เ3เ4เ5เ7เ9เ;เ?แBแFโJใNใQไUๅZๆ]่a้d๊f๋i์lํn๎o๏p๐q๐q๑q๑q๑p๑o๑n๑l๑j๐h๐e๏c๎`๎^ํ[์X์U๋R๊P้M่J็H็EๅBไ@ใ?แ<เ:88ุ7ี9ะ?ษ๕Iย๋–]ทุQฝใ?ทฺEฒี9ฝ฿ฆ1ฤ่.ห๒/ะ๘0ิ3ุ6ฺ8:;฿=เ=แ=โ=โ<โ;ใ:ใ9โ8โ7โ5โ4โ3แ3แ2แ2แ3แ4แ6แ8แ;แ>โAใEใJไNๅQๆV็Z่^้a๊d๋gํj๎m๏n๐p๑q๑r๒r๒r๓r๓q๓p๓o๒m๒k๑i๑f๐d๐a๏_๎\ํYํV์S๋P๊N้K่H็EๆCๅAใ>โ=เ:98ื8ำ<อ๛Eฦ๑่Nภ่;Lย๊=นิ9ผ;บ^4มไ๘.ษ๎-ฮ๕/ำ๛1ึ3ู578฿9เ:แ:โ:โ:โ9โ8ใ7ใ6ใ5โ4โ3โ2โ2โ1โ2โ2โ4โ6โ8โ:ใ>ใAไEๅIๅMๆQ็V่Z้^๋a์eํh๎k๏m๐o๑q๒r๒s๓s๔s๔s๔r๔q๔p๔n๓l๓j๒g๑e๑b๐_๏]๏Z๎WํS์Q๋N๊L้I่FๆCๅAไ?โ=เ;9ฺ9ึ:า@ห๗Iฤ๎XฝใQภ่=นู?ทึ7ฝ฿ำ/ล้-ฬ๒-ั๘/ี0ุ2ฺ467฿7เ8แ8โ8โ7โ6ใ6ใ5ใ3ใ3ใ2ใ1ใ1โ1โ1โ2โ3ใ5ใ7ใ9ไ=ไ@ๅDๆHๆM็Q้U๊Y๋]์aํe๎h๏k๑n๑p๒q๓r๔s๔t๕t๕t๕s๕r๕p๕o๕m๔k๓h๓e๒c๑`๑]๐[๏W๎TํQ์O๋L๊I่F็DๆAไ?โ=฿;:ู:ี=ฯEศ๒้Oย้=Mร๋Aดึ\ ฤ:บˆ2มไ-ษ๎,ฯ๕-ำ๚.ื0ู134฿5เ5แ5แ5โ5โ4ใ3ใ3ใ2ใ1ใ1ใ0ใ0ใ0ใ0ไ1ใ2ไ4ไ6ไ9ๅ<ๅ@ๆC็G่L้P๊T๋Y์]ํa๎d๐g๑k๒m๓p๔q๔r๕t๖t๖t๖t๖s๗r๖q๖o๖n๕k๕i๔f๓d๒a๒^๑[๐X๏U๎RํO์L๊I้G่DๆAไ@แ=฿;;ุ<ำAฬ๘Kฦ๏™ZฝใRย้;ธุ=ทื65ฝๆ.ฦ้,ฬ๑,ั๗-ี.ุ/012฿3เ3แ3โ3โ2โ2ใ1ใ1ใ0ไ0ไ/ไ/ไ/ไ0ไ0ไ2ๅ3ๅ6ๅ8ๆ;ๆ?็B่F้K๊O๋S์Wํ\๎`๐c๑f๒j๓m๔o๕q๖r๖s๗t๗t๗t๗t๘s๗q๗p๗n๖l๖j๕g๔d๔a๓^๒\๑Y๐U๏RํP์M๋J้G็DๅBไ@แ>฿<<ื?ัGษ๔โPล๋5Nฦํ?ดิEฏฮ8นูœ1มใ,ษํ+ฯ๔+ิ๙,ื-ฺ./0฿0เ1แ1แ1โ1ใ0ใ0ใ0ไ/ไ/ไ.ไ.ไ.ๅ/ๅ0ๅ1ๅ2ๆ4ๆ7็:็=่A้E๊I๋N์RํV๎Z๏_๐b๒e๓i๔l๕n๖p๗r๗s๘t๘t๙t๙s๙s๘q๘p๘o๗m๗j๖g๕d๕b๔_๓\๒Y๑V๏S๎PํM๋J๊G่DๆBใ@แ>=ฺ>ีDอ๙Mฦ๐†`ธเSม๊:ทื<ถี>4ผ๊.ฦ่+อ๑*า๗+ึ+ู,-./฿/เ/แ/โ/โ/ใ/ใ.ไ.ไ.ๅ.ๅ-ๅ.ๅ.ๆ/ๆ0ๆ1ๆ3็6็9่<้@๊C๊H๋L์P๎T๏Y๐]๑a๓d๔g๕k๖m๗o๘q๘r๙s๙s๚s๚s๚r๙q๙p๙o๘m๘j๗g๖e๕b๔_๓\๓Y๑V๐S๏PํM์K๊G่EๆBใ@เ?>ุAาIส๔ะRฤ๋%Pฦ๎>ฒำDญอ8ธุ›1มโ+ส์*ะ๔*ี๙*ุ+,,-฿-เ.แ.โ.โ.ใ.ใ.ไ-ไ-ๅ-ๅ-ๆ-ๆ-ๆ.็/็1็2่5่7้:้>๊B๋E์JํN๎S๐W๑[๒_๓b๕f๖i๗k๘n๙p๙q๚r๚r๛s๛r๛r๚q๚p๚n๙m๙j๘g๗e๖b๕_๔]๓Z๒V๑S๏P๎M์K๊H่EๅBโ@฿?@ึFฮ๘๗Nศ๏eํUร๊:ถี<ดา74ปใ-ฦ็*ฮ๐)ำ๖)ื๛)ฺ*+,,฿-เ-แ-โ-ใ-ใ-ไ-ๅ-ๅ,ๆ-ๆ-ๆ-็.็/็0่1่3้6๊9๊<๋@์CํH๎L๏P๐T๑X๓]๔`๕c๖g๗j๘l๙n๚p๛p๛q๛q๛qq๛p๛o๛n๚l๙j๙g๘e๗b๖`๕]๔Z๓V๑S๐P๎N์K๊H่DๅBแAAูEาMห๓ฌUฤ๊Rว๎=ฒฯFฉย9ทึ†1ภแ+ส์)า๓(ึ๙(ู)**+฿+เ,แ,โ-ใ,ใ-ไ,ๅ,ๅ,ๆ,ๆ-็-็-่.่/้0้2๊4๊7๋:์>์AํE๎I๏N๑R๒V๓Z๔^๕a๗d๘g๙j๚l๚n๛opppponm๛k๚j๙g๙e๘b๗_๖]๔Z๓V๒S๐Q๎N์K้H็DไBเBDึJฮ๗฿Rศ๏8Nษ๓jษุ;ติ=ณั"6ปฺห.ฦๅ)ฯ๏'ี๖'ุ๚(ฺ()*฿+เ+แ,โ,โ,ใ,ไ,ๅ,ๅ,ๆ,ๆ,็-่-่.้/้0๊1๊3๋6๋8์;ํ?๎B๏G๐K๑O๒S๓W๕[๖^๗a๘d๙g๚j๛kmnooonmlk๛i๚g๙d๘b๗_๖]๕Z๓V๒S๐P๎M๋J้GๆDโCDูIั๚๘Oห๒qbตืSฦ์>ตา/ป฿:ถีU3ฟ๏*ส๊'า๒&ื๘'ฺ(()*฿+เ+แ,โ,ใ,ไ,ๅ,ๅ,ๆ,็-็-่-่-้.้/๊0๋2๋4์7ํ:ํ=๎@๏D๐H๑L๒P๔T๕W๖[๗^๘b๙d๚g๛iklmmmmlkjh๛f๚d๙a๘_๗\๕Y๔V๒S๐PํM๋J่GไEแDHีNฮ๖ฆUว๎Sส๑?ฒัFซส8ธืŠ/ฤโ(ฯํ%ี๔&ุ๙''()฿*เ+แ,โ,ใ-ไ-ๅ-ๅ-ๆ-็-็-่-้.้.๊/๋0๋1์3์5ํ8๎;๏>๐A๑E๑I๓M๔P๕T๖X๗[๘^๙a๚d๛fhijklkkjhge๛c๚`๙^๗[๖X๔U๒R๏OํL๊IๆGใFGุMั๘ฮSฬ๑*Qอ๓>>>>;ตำ>ฒะ5ผฺฒ+ศๅ%า๏$ื๖%ฺ๚'((*เ+แ,โ-โ-ไ-ไ-ๅ-ๆ-็-่.่.้.๊.๊/๋/๋1์2ํ4ํ6๎9๏;๐?๑B๒F๓I๔M๕Q๖T๗X๘[๙^๚`๛cefhiiiihgfd๛b๚`๙]๗Z๖X๔T๒Q๏N์L่IไGเHMำ๚ๆSอ๓KGฺVส๏yŒˆˆ{ƒo—‹ jŸ!kข’Fjฅ“giง•}hจ–‹gซ—Ždฌ˜‡bฎ˜wbฏš\bฐœ8cฏ›wญœpฎœIกร6พฺ<ถำ,2ภห(ฬ่#ิ๑$ุ๗&๛'()฿+เ,แ-โ.ใ.ไ.ๅ/ๆ.็.่.่.้/๊/๊/๋0์0์2ํ3๎5๎7๏9๐<๑@๒C๓F๔J๕M๖Q๗T๘W๙[๚]๛`bceffggfedb๛`๚^๙\๗Y๕V๓S๑P๎M๊J็IโHLี๒Rฯ๕j^ภVห๎b™‰e•†_Š&]ขl\ฅฎ\จ’Zซ”๑Yญ–Wฏ—Tฑ˜QดšLตšHถ›Bธ›๘?ธœ์>ธœฯ=ธœ˜?ธ›UCถ˜oจ–Xฏ—?ญห&ึ๏8ธิ>/ร฿ื%ฯ๊#ึ๒%ู๗&')฿+เ-แ.โ.ใ/ไ/ๅ/ๆ0ๆ0็0่/้/๊0๋0๋0์1ํ1ํ3๎4๏6๐8๐:๑=๒@๓C๔G๕J๖M๗Q๘T๙W๚Z๛\^`bcddddcb`๛_๚]๘Z๗W๕T๒Q๐O์L้JไJ฿Lุ๘Qั๗ƒZว์Vห๑Q—M‰uSž‡)Tฃ‹‰SงRซ’๛Qฏ•Qฒ—Pด™PตšOถ›MธœJบEปž>ผž7พŸ/ฟŸ'ฟŸ#ฟž#ฟž๖$พฯ&ฝ*ปœ01ถ™3ธฌ6ปึF,ฦเฺ#ั๋#ื๓%ฺ๘'(*฿-แ/โ0ใ0ไ0ๅ1ๅ1ๆ1็1่0้0๊0๋0๋1์1ํ2ํ2๎4๏5๐7๑9๑;๒>๓A๔D๕G๖J๗N๘Q๙T๚V๛Y๛[]_`aabaa`^๛]๙Z๘X๖U๔R๑P๎M๊KๆJเMฺ๚Qำ๙‘Wฬ๓Uฯ๕IŸ…Hž„ LฃˆhMจŒืMญ‘Lฒ•Lด—Kต˜Kถ™Kท›KธœKบKปžJผŸHฝ Dฟก>ภก7มข.ยข%รขฤขฤกร ยŸโภž–ฟž<บœ4พุD)ษแิ#า๋$ุ๓&๘(*฿.เ0แ2โ2ใ2ไ2ๅ2ๆ2็2่2้2๊2๋2๋2์2ํ2๎3๎3๏5๐6๑8๑:๒<๓?๔A๕D๖G๖K๗M๘P๙S๚U๛XZ\]^___^]๛\๚Z๘X๗V๕S๒P๏N๋L็KโM๚Rิ๙–Vฮ๒Uั๕BŠrIณ”EžƒGฆ‰›IญŽ๕Hฒ“Gด•Dต—Cถ˜Bท™BนšCบ›DปEผžFฝŸFพกFฟขEมฃBยฃ>รค9ลค1ฦฅ(วค ศคศคศฃวขลก้ฤ กยŸCป™ 3ม5'หใร"ำ๋%ู๓'๘)-฿1แ3โ4ใ4ไ4ๅ4ๆ4็3็3่3้3๊3๋3์3ํ3ํ3๎4๏5๏6๐7๑9๒;๓=๓?๔B๕E๖H๗K๘M๙P๙R๚U๛WXZ[\\\[๛Z๚Y๙W๗U๕S๓Q๐N์L่KใN๗Rึ๙Vะ๑Tา๔;˜wJถ›?ก‚,Cฉ‰นEฐDด“@ต•<ถ–:ท—:ธ˜:บ™;ป›<ผœ=ฝž?พŸ@ภ BมขBยฃCรคBฤฅAลฅ>ฦฆ:วง3ษง+สง"หงหฆหฆหฅสคศฃํวขฉล K(รส4%อใฆ"ิ๋๘%ู๒(๘+1เ5โ7ใ7ใ7ไ6ๅ6ๆ6็5่5้5๊4๋4์4์4ํ4๎4๏5๏6๐7๑8๑9๒;๓>๔@๕B๖E๖H๗J๘M๙O๙Q๚S๛U๛V๛X๛X๛Y๛X๚X๙W๘V๗T๕R๒P๐N์L่LใN๐Rึ๙~Yั๔ Vำ๖9—zCท=ฃƒ0Aซ‰ฤCฑ?ด’:ต“6ท”4ธ–3น—4ป™5ผš6ฝœ7ฟ9ภŸ;ม =ยก>รฃ?ฤค@ลฅAฦฆ@วง?ศจ=ษจ:สจ4หฉ.ฬฉ&อฉฮฉฮฉฯจฮงฮฆฬฅ๑สฃฒศญj อู’"ิ๊่&ฺ๒)๗.฿5แ9โ:ใ:ใ9ไ9ๅ8ๆ8็7่6้6๊6๋5์5ํ5ํ5๎5๏6๐7๐8๑9๒:๓<๓>๔@๕C๖E๖G๗J๘L๘N๙P๙R๚S๚T๚T๚U๙U๘T๗S๖R๔P๒O๏M์L็MโO฿Sึ๙aZฯ๑Vา๕/‹n?ฐŒ9ค‚*?ฌŠร@ณ;ต‘6ถ’2ท“0น•0บ–0ผ˜0ฝš2พ›3ฟ5มŸ6ย 8รก:ฤข<ลค=ฦฅ>วฆ?ศง?ษจ?สฉ>หฉ<ฬช9อช5ฮซ0ฯซ*ะฌ#ัซัซาซาชาฉัฉะง๔ฯจฤฯมฐิแ฿%ู๐*๖2เ๛9โ<โ<โ<ใ<ใ;ไ:ๆ9็9่8้7๊7๋6์6ํ6๎6๎6๏7๐8๑8๑:๒;๓=๓?๔A๕C๖E๖G๗I๗K๘M๘N๘O๙P๘Q๘Q๗Q๖P๕P๓N๑M๎L๋LๆMเPพTึ๙=eภ์Xั๗9จƒ7ฆ‚=ญˆถ>ณŽ8ต1ถ‘.ธ’-น”,ป–-ฝ˜.พ™/ฟ›0ม1ยž3รŸ5ฤก7ลข8ฦค:วฅ<ศฆ=ษจ>สฉ?หฉ?ฬช>อซ>ฮซ<ฯฌ9ฯฌ5ะญ1ัญ,าญ'ำฎ"ิญีญีญึญึฌึฌิซ๗ำฒ้ิฬ๖"ู็,๕6เ๚<แ>แ>แ>แ>โ=ไ=ๅ;็:่9้8๊8๋7์7ํ7ํ7๎7๏8๐8๐9๑:๒;๒=๓?๔@๔B๕D๕F๖H๖I๖K๖L๖M๖M๕M๔M๓M๑L๏K์K่LใN๊Qฺ๛‡Vึ๘=๋]ะํ6จ‚3ฃ~ ;ฎ‡˜;ด4ถ.ท+ธ’)บ”)ผ•*ฝ—+ฟ™,ภ›-มœ/รž0ฤŸ2ล 4ฦข6วฃ8ศฅ9ษฆ;สง=หฉ>ฬช?อซ?ฮฌ?ฯฌ?ะญ>ัญ<ัฎ9าฎ6ำฏ3ิฏ/ิฏ+ีฐ'ึฐ"ืฐุฐูฐูฑฺฑฺฐูฑูพฺุ*๎8เ๘?เ@฿@?฿?แ?โ>ไ=ๆ<็;่:้9๊8๋8์7ํ7๎8๎8๏9๐9๐:๑<๑=๒?๓@๓B๓C๔D๔F๔G๓H๓I๓I๑I๐I๎I์I้JๅLเ๖OตRุ๙EXะ๑Uิ๕5ฃ}J฿ฎ8ญ†a9ณ‹๕1ถ*ท'น'บ’'ผ•(พ–)ฟ˜*มš+ยœ-ร.ฤŸ/ฦ 1วก3ศฃ5ษค7สฆ9หง;ฬฉ=อช>ฮซ?ฯฌ@ะญ@ัฎ@าฏ?ำฏ>ำฐ<ิฐ:ีฐ7ีฑ5ึฑ2ืฑ.ืฒ*ุฒ&ูฒ#ฺณดดตตตธว$฿฿6฿๑?๗@ู๘@ฺ๛@@?แ?ใ>ไ=ๅ;็:่9้8๊8๋8์8์8ํ8๎9๎:๏;๏<๐=๐>๐?๑A๑B๑C๐D๐D๏E๎F์F๊F่HไJเ๘MฤQู๙`Uี๖:์]อ๊3ฌƒ3ช6ฑˆศ0ถ‹'ท#น#ป‘$ฝ”&พ–'ภ—(ม™)ร›+ฤœ,ลž.ฦŸ/วก0ศข2ษค5หฅ7ฬง9อจ;ฮช=ฯซ?ะฌ@ัฎAาฎAำฏAิฐAิฑ@ีฒ?ึฒ=ืฒ<ืฒ9ุณ7ุณ4ูณ1ฺด.ด+ต(ถ%ท"฿ท เธแนโบโบใพโห0๙@า์ํBะ๐Aี๕Aู๚@@?เ>โ=ใ<ๅ;ๆ9็8่8้8๊8๋8๋8์9์:์:์;ํ<ํ=ํ>์?์@๋A๋B้C่DๅFโI฿๑LบOฺ๚aTื๗cรโXั๑+ฎ+ฌ"(ฑ…"ต‰ธŒบ ฝ’"ฟ”$ภ–&ย˜'รš)ฤœ*ล+วŸ-ศ /ษข0สฃ2หค4ฬฆ7อจ9ฯฉ<ะซ>ัฌ@าฎAำฏBิฐCีฑCึฒCืณCืณBุด@ุด?ูต=ูด;ฺต9ต7ต4ถ2ท/ท-฿ธ*เน(แบ&ใป$ไฝ$ๅพ#ๆพ#ๆฝ$โฝล>ฬูDEวๅCห้ฬBฯ๎๓Aา๓@ึ๘@ฺ๛?>=เ<โ:ใ9ไ8ๅ8ๆ8็8็8่8่9่:่:้;่<่=็>็@ๅAไCแF฿๗IืKฺ๚•Oื๘HRี๕nภ๋Zฯ๓!ฌ!ฉ|ฏ‚Zณ†สทŠ๙ปŽฝ‘ฟ”!ม–#ร˜%ฤš'ลœ(วž*ศŸ,ษก.สข0หฃ1ฬฅ4อง7ฯจ9ะช<ัฌ?ำฎAิฏBีฐDึฑDืณEุดEูดEูตEฺถCถBทAถ@ท>ท<ธ:ธ8฿น6เบ4แบ2โป0ใผ.ไฝ.ๅพ-ๆฟ-็ฟ,ๅฝ๗,เธg.์MณัGร฿FวใNDษ็Cฬ๋หBฯ๏๎Aา๓?ี๗>ุ๚=<:9฿9เ8แ8โ8ใ8ใ9ใ:ใ;ใ<โ>แ?เBD๎G๛ฮIฺ๙–Lื๘XOี๕Vั๏Sา๒-ฐ…4ญ‚)ฑ…&ต‰g"ธŒมปŽ๖พ’ม•ย—!ฤ™#ฦ›%ว'ษŸ)ส ,หข.ฬค1ฮฆ3ฯง7ะฉ:ัซ=ำญ@ิฏBึฐDืฑEืณGูดHฺตHฺถHทHธHธGนEนDนBนA฿บ?เป>แป<โผ:ใฝ9ไพ8ๅฟ8ๆภ8็ม9่ม9็ฟ7แน 7ุฑ6ดLภNพุHรเFวๅ=Dษ็pCฬ๋Bฮ๎ฦAะ๐ใ@ำ๓๏?ี๖๗=ึ๗=ุ๙<ุ๚<ฺ๛<ฺ๛=๛=๛>๛๗@ฺ๚๏Cู๙โDุ๙ยFื๘™Iื๗lKี๕ฒˆXช‚3ตŠ+ธŒX&ปณ!พ‘๐ม”ร—ล™ ว›"ศž%ส (หก+อฃ.ฮฅ1ะง5ัฉ9ำซ=ิญ@ึฐCืฑEุณGฺดIถJทKธLนLบL฿ปK฿ปJเปIเผHแผFโฝEโพDใพCไฟBๅภB็มB่ยD๊รF๋ลF๊รBไฝผ?ถ!@฿ธ`ชฑLฝูGฤโFวๆ$Eส่?Eฬ๋XDอํlCฮ๎zCฯ๏‚Bั๑…Cั๑ƒDา๒{Eา๒mGา๑YIา๑?Jำ๒#Mั๏Uห่AโJฎ‹69ต0บJ+ฝ‘ฅ%ฟ“๊ย–ล™ว›ษž!ห %ฬข(ฮค-ะฆ1าฉ7ิซ;ีฎ@ืฐCูฒFฺดIถKทMนO฿บOเปPแผPแฝPโพOใพNใฟNไฟMๅภLๆมL็ยK่รL้ฤM๋ฦPํวS๎ษQ์วJๅภวEธ.F่รFะซl+%ฟ—;ถŒ 3บ=.ฝ’˜(ม•ไ"ฤ˜ว›ษห !อข%ฯฅ+ัง0ำช7ีญ=ุฐCฺฒGตKทN฿นPเบRแผSใพUใฟUไภVๅภUๆมU็ยU่รU้ฤU๊ลV๋วWํศY๏ส\๑ฬ]๑อX๎ษOๆมรHน2P๑หEิฑ:ถŽ>ณ6ป‘21พ”‹+ย—%ลšษหŸฮข"ะฅ'าจ/ีซ6ืฎ>ฺฑDดJธNเบRโผUไพXๅภZ็ย[่ร]้ฤ]๊ล^๋ว_ํศ`๎สb๐ฬd๒ฮf๓ะg๔ัd๓ฯ[ํษ๛PไภฐIท(V๗าEัฌ=ทCฒ‹6ผ’)2ฟ•-ร˜ี'ว›๛!หŸฮขะฅ$ำง+ีซ4ุฎ<ฒDถKเนQใฝVๅฟ[่ย^๊ฤa๋ฦcํศf๎สg๐ฬi๒ฮk๓ะm๕ัm๖าj๕ัb๑อW๊ล๋Nโฝ…HืตK็ฤCซŠ?ถŽIฎˆ8ผ“#4ภ—u0ลšฮ*ษ๙$อก!ะค"ำง'ีซ/ุฎ8ฒ@฿ถIโบPๅพW็ม\๊ฤa์วd๎ษg๐หi๒อj๓ฯh๓ฯe๓ฮ_๏สV้ฤ๖OโพถIธHDิฐGูด?ธ‘Lซ‡:ฝ”6ย˜i2ฦœภ-ส ๓(ฯฃ$ำง%ีช)ุญ0ฑ8ต@แนHไผN็ฟS้ยW๋ฤY์ฦX์ฦV๋ลR่รNไฟ๎J฿บดGฺถYBำญn๎:ษฃCบ‘Mฒˆ=พ–8ร™O5วž1ฬก-ะฅ๗*ำจ*ึซ,ูฎ0ฒ5ด9เท=แธ@แนBแน๛B฿ธ์BถวBดˆAืฒ@>ะญ!zf:รก=พ–>ผ”;ลš#8ศŸW5หขˆ1ฮคฌ/ัฆฤ.าจะ/ำชะ1ิฌฦ3ีฌฐ6ีญ‘8ิญi:าฌ9;ะช=รœ<ศกIทTฐŠ;ภ™ 5วž2ษ 2สข3ษฃ7ฦ  =ม–;ร™ภ๘ภ๘€?เ?€???๘๐ภ€๘๘๐เภภ?€๘๘๐๐เเภภภ€€๘๘๘๘๘๘๐๐๐๐๐๐๐๐๘๘๘๘๘๘??€€ภภเ๐๐?๘๐|€€๘๐เภ?€เเ๘€เ?๘€๐‰PNG  IHDR\rจfirIDATxฺํฝy %IU&ˆผ๏ฝZบ›nถVDDฅบ•lVป7†AtDว๕W:n(หฐถ (ศR=€ฺะ,‚ Šโศโ "3ฌ‚,BwWฝ๗๎อŒ๓๛#32Oœ8‘๗พชW๕ช๛ๅฉบ/3#"#ท๘ฮ๙ฮ‰ˆL`’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&9ำ„๖๚๖JŽ~่าตkพ๚๙ ๆw๔>–|s+Oฎ9‹เภ˜t-1} pŸ ๐Ÿ‚ฃฏี็~่ฒป_๕yxฏฏc’INF๖“ ผ็พ฿บ๊Kˆ๘ปฝ ๗tUsุ๙็ฺƒํข๖™&0;ๆเ?รLj๗.Gv้฿๑šย$ื3นม+€]}ั7›`UแQ7๚Y ็š๔.ภ@p8r "๙nฺ้u8คส`B4 ‚๛dh่MซืใŽ7~ืetEณืื>ษ$หไฉŽฝิ๋M>๗=•ฏŸTUีZํซแ|ำ‚Ÿ๐‰เตภwŽ<ˆิา๙VภuK๊o3ทฟภhBƒฆfิ5_h่ตฬwq‡ทdฏ๏ว$“”ไฅ~ํOฟ๓œณชญว{W?ฅZknํ} ๏[‹Oฎ?wŸเศwภฏเจ‚sณ6ญnBี+ู๊Aงฺ`f ฬMำ ฉk, ื ๓ฆฆ็,๑ผ+/ปlb“œYrƒPฯป๊ขฏs๋๓ง๛j๑๘ูZsŽ๗ œ‹ภ๏–„๘nฐ๎nึฟ‚ฃYปํชN ฬz๋?ฐŽ$Š 0เๆ!4กF]ื˜oฯฑ˜‡šื+๗…๚UG.~gฝื๗l’I€๋นxฮ›๏๓-ณ๕ๆฎj~ฐช๊๕ึฺื่๋ึฺ;ni~็หปฤฺwKฉข๕'็ผRŽะืื: 0€ะบhภ! „Mณ@]ฯ1฿ฦึv!เนLAรI๖Vฎ— เyW๋ฮnซฏ๊G๚ช๖/ะปŽธ?uเ”^‚–^.ฃ๏๏ข๏฿[ ``ิ๔Š€จ‹ D6ภญจฺฺุยึผ~+P=๙{n๛ฆ๏๕œdส๕J<็อ๗๙w~ญ_-่ซ†ผ[€ขณ๖ฑ+/๖\gฑะSb๙%๐ฝ๐๙่@o€‘๘9ขฎ๋†<ัศ37\#„๕๓ํใฺุพnพีƒพๅ-c๊Bœd/ไŒWฬ ็ผํCU…๊}}__-เ||j@:ขOไ\ ๖ุญUทŸFใพ>ต๖ˆ๋่ถ‘)€v์€SiยMhฏŒ่ฬฑXlas๋6/ˆ๋๐ธๅอ_ุ๋๛=ษ’3Vน๚ข๊์ญ๙๗Vณ๚งชjqg็็pพัข ๊uwZ๋Kฎ[๚$‚O๔Fd?ฦrz๏z‹ž:KB๎~ภTRHw]ื!Bฯถทc๓ุๆงถถ#rม?ฐื๗~’#gœ๘ํฃ๗:เn„vh~าฏo฿ฦนœ›ท>>…๘-่Z฿ฅA>๒๕—`AฝชSข[ฺŸV๕@†ุGั)… F๖€O‚ฃvิกๅmทแ๓๙&Ž?~|kป๙มKพๅ-oุ๋็0ษ3F<๛ ›OrU๓cีl5ฮฯA4๏่}ˆŸ ิ< ึ๙DK๏<า=N๒:๊ž‚6Zz๊ม>(ไห๎ucz% %3ธํ@ข‹z วo67›ง~ฯทผๅ…{L&นแหž+€฿พ๒^ท@žFณ๚ ณj๓œ6’ฟQG๓ฉธpƒต–>UูhพŒ+ซ฿ƒ]Xl1ุGฌ…ภข๛/U ƒBเ”!dlร๕กesln็ใื…Ÿ~๐ทพๅ7๗๚๙Lrร–=Sฯz๋E฿๊pO:_ ๗๋‘ๆลn<๔@t™?z€รCSH๋I๖Ž๒๗i!z’หดO”B›F2]F๖.Aw>ฬMจฑตน…ญอๆบํžตWฯh’พœv๐์+๏}/^~†๗๕Cศoz็ๆ่ใl<ีตฆ-fโณk‹ž[{‡กไำภ^b๕ำ@_{sไH‹ูภ,ฤ&วhB$‰ยAWk ถทถxs“Ÿเ้‹O๗sšdศiQGŽภu๗{<พ~†ฏถ๎Knซ๋ป—ึ^฿} ž Š=ศๅ >้ืหmJึ%๐a(€ๆง7‰(=ภ#H‚NAะGPŠ gนKะ*€ํญyณนษ—^๒-ถงA่Šฃ—บ;฿๙บ๊œœ๙„zฦื-ฎY|จ/บ่อ4žแ๚%งT9zมฺแƒ‡j๛'฿บฝsญo฿ ุqฦ`ฤจ~?จFนbศ)พdN€‰Hฝ~๏๋๋%”ี"๕Oญ;``้๚~™ฎ]ƒุ{0L;fPซถ๋c››k=๔[๔}ง๊Yฝ๏%wทธh๑อ p ธฅ๗๘z฿ฤ7แฦ>LŒ5ช@ฤLิพ\กัuพฬ์ภ็๗ฉ@๎Sิะ?9<็ŽŸฆiš๔%งD<๏ช{žฝ๊์๖ำœ฿ผ%น95ธ|ไ>‰พKภง้(•Aห\โืJมล<ค2€(ญ[ภ'u“$ภปI@ +@d ]นžDe00ƒ^$ฌ``(ฑฦถ7๑™k6g๗ธ์ยw~๎dŸฯ๛^rืูM๎pณW!“<฿อ๎DŽo็]8่]œ?ัอšฎิ+ฐVกEฦิท$Ž็ฝ3ญ†์ภ์เฎแเž™>r๏ฅ@sึ๏๒1ข#แTดรI–หฎ*€#G๏~ฦฦ๖Q5’s›็:ท9๎^ดป๏ ฺ @๙็UOXภเ˜t_ฐd๕_< ๖‘ฐฺ1Y$เ#ศRะฅ%ึ_ฐ0ิวJ–„ภŒฆalmนwภญฐำiลG^๊๏๙5ว๏ไ\ พๅ๘ฝ็ณฝ pŽแ#เาeผr ็[ภทฯG€ฝบsƒ›าฆะLT€๘ๆm%ะŠ๋nY็7t๋ฬZ–‚G๗Žš[6ร7฿ฏnดัIl9ipไŠ[ทซ๊W๚ชนi฿ฐบYx้ˆพ์rKŸZ{ไ€ิ=ต่i\ ๏ฺ3}Hำˆขศ)>hŒh+/ืrW`(-m{ด›B`,tl~์ภน๐Ÿึฯโณ๏ปไเึๆโม๑๗y8๓แ ๏๏๘~iํฃซ]’ ๐"˜iถ‚‘! ณZr่B่Ž มกiช†ƒ{Oช7,ๆ๔บs๏๒๚Ol{$•“R?}๔ฯ9PัgfU8+พcฏmPศiฟฆ๘ษv{*รค5('‹ฺ#yไ3๚๚E€/น์๓GงTไ?Q ฬ6ุ[%Ad(…ฮ๚k— มM`ฬท๓t›1 ,๕ป๏U9<ฮ{~ไฌ 7jAด_ฟ,|w-$˜Mฒ.—ฒี0าŒ็h๔›JะGภG้•@ทฎ~ฺ๘AT!4๎ฏjฎ^เฏ8็ย+พ| qฑoไไภ+๏p‡ณฯ๒็}ข9ˆบiน$tึ่-0รH,u๎ฟปD1๔ƒy”+`)dูQž‰$ ^๖€<™๛ศภ(ฎƒ:ะษ4ฉฺ}เงมมxh!,่รท๙ฤ๎ร~๛‡*฿ˆwแยYเ=ฃ๒่ม๎อwัญpะ5๘๛k”Kฃล˜, ZwqลbzTย5่B !84มกYTวWฏ_ฐฟฌ ็ปงฎว—“RG^vัฦ์œ๋>7[w็‰Jaู ห?๘็9ล7gเ €ค_?๕๕ำAZw่ƒ0dRฤซิ. z๔0G๐K๋๚etาnยะ๛i_0ฒ›.nt์œฏ4PMgybTž:฿^}ฏ I'๋โzz€J@’๕—…ˆ๚"นๆะฑ.MณS-3h&Tไฆz๎ฦมรฏฆ[ฟ|kw`ฑไคcฟt๔ฎฯ9ppใiŽj€ถด#๛๚้บฤยOŸLง•”_Yzด~์ฉWtap%๐)mหV๔_ัยƒม่=ลŠ๒Ke&ะ๕a`Q9H” ‡ุใk๘lœว็kเE J฿พแธe`ล‡M๑%  ฑmดซี•IiG—€์สXค% ƒR`๊nIง‚Ch<lฬž{hถb๚ๆ?ผๆd๕~‘“V?๛;๗ผ๙แใCk๋k7&šุฐ่”€˜ฬ‰@Oํ๛4m้GY ฐงฎB–ืฅ๗ ๏ฎ>eฒ‹กพˆ้ภŸๆ๕VฝI๒Zk฿$l€‹มBฉ\€€ร๐ธŸ…s้Tt* nAเ˜Tนจh7ส`/ญ๗ื+[งt>ฃ†•—ึŸ,ภหหR,ีz ๎ฃ shZEภมกฎซ/.ู๊๊qณ็ั…/ผ๎d๗ ]NZภ/ฝ๊ฎY?ธ๑๒ูฬกesuช€T๔ิ”R%ฐิ๒งV๊Lcr^`๑ญ€dv_!ฐ—๙"๘›dIhZะS.ฟ ศบฤฑบ฿!&|ใ<:Œ™;Oเุƒ:ห'qํสฺ#๗๏ฅEทฌพฟnPฬศ2ะ๗7Vƒ_—“ @ป 1‚๐ur@วZeภวผž}nQำ/บ๑ๅtทหปัฮoˆฒ+ Žผๆ๎ฏ>t๐ภฃผg €ๆ ดฎ@คพ€’„Rv @{šถRฎBชา๔แ8่ทaถลิ๊หม;่‘๒Gp‹%ฺ–pผn‘ืo๗ฎถb˜ฑฮ„[โnๆcๆfจ:ิีำแ5Ÿูญ6C]Spไ๗๏q๊€ฯ็9@hั๔พoŒz—]AแG‹ฤT๔_ปˆํ–ป2ส๒“๒๋ฅ ๏ม฿ั}ฑdhš/ฌ~’3ธแpำ0ใo\;์๚๘ญuเ๏f-.k *Ÿ?^q(๒๒A=เญuJฑ›Y๖X†า%DYญบเlrฑ 1‰ _ฦ๕ Bรร-oข[เP/5๕์g_xม‹ฆ๙ญ์ช€Ÿ้=๎๐๐ฺUึึ€jฤx0๔ศ‘oƒ  A;๚_ค๘คส )ำีQtŠฏม—tฟฝŽ]ธO๏@.[&wV?pอCจ}๊เmฮฉึo=๓3Tร“ƒwUo๙“oสnทHฌ?ไบศณtT฿Hฐฏฤ$ “x€Nsyy3้R0@— Al {<Cจ=๕์jๆูc\๐ชOํv๛ฟพษฎ+๘ล—฿ใัฮ^นพฑแฺฉฟ €ญh„KภI;ฑz ฬศ~B๋sเ'}€ศ๏.Z๛ †‘{ฉO฿Ÿฺk ็D„๐ƒต็๘}N84๔%฿ธห๏Pr็ฬช]ซf‡fร;ืƒ฿ัZ๗2S`xeXไ4ภ ๋ึˆพ„๚ว›มCฆๅgพ“ฅb—‚บ๕™fฆ œ Bj๑ม่ั-{w ฦธc‡ลข๚สผ™่แ _๛‡งื9% Žผ์>O8ไ_ธพ>#็4๕เ DŸY •ฬ1 ™€TึE ฝัl•Pคƒsze…ศb"ะ[—†Qc0-,…5สz๗isพ็ูM6ยsgž๎ทVyT.‚๘‚‘w3yไ[†UgKสวทฌ‘K%0B๗3๐ซๅ(๐ใบ+ŸLฦข;$้?‹ฅ`’ 4<ฤ‹zํe›7๚QบๅวOฮd9e Žผ์>O฿8T๖ฺ๚ŒผC๏ ไ]`0N›๎["Jฑ `XŠม>I .*จFX๎ื_๚๚-๐นซหโ“แja 7ฟ๔ีs_ภ›๘Kืg[ฯ_Ÿนsf•Gๅฺw๘๎๓ไžฺ”๙มb๊'”Ynt —ภ๑๏-%ไ‹ฬ๗ทh>€น๗้ีHค$ฟ 2F!N‚ี–?nGเ—”@รส% pํ0ŸWžฏ_zฮ_๕ฑS‰‡3QNฉ€_|ูwุ๔œ๕5็\W๛HŽ’SoสัCw‡nA ๙BOTฦ˜‹๚ห€_๊๏ว๓Š1‹L] 3ZŽ ศฏฃ]๋ภppใ฿รpฟ‰{้ส‡ฟํŽh๛y๋[[Ÿ9Tพk๙ค8๑*๓๊๑˜เ้ฃ GพFึŸ_6ธK@K‹qซ ‘ญ“ํถG€RA(ƒ่Dฏญธvจณ/ฯ›๊ั‡๏๐ฺทํ*ฮp9ๅ Žล?ผพ฿]?Pอ|Elด่_}ๆipฐ;Aญ2งŠ`P1†ๅว‰๋ƒO฿v[ึ`ิ ,ภฝ฿/„่(?38ฃqom๋ฟุ‹?๗.ฎพืm่เ—็ฦฦ๖ทญU„สyT>R~๊†๎:8ฬ@ฎ๓๙-P๓Nภ^๔๛##ฑ๑a่€Ÿ ๔H็ล:iฐฏ0šฉ์ํภภภ๚X€ฏิ/ฦj‡ลถฏท›๕งŸuวืผเTcโL‘ำขเ/ฟ7ัซืื9ีฬ๕๓ะ[ะษe๚ถ9u7๏ Dโ&๔.AฏbYˆzปHH๛kA๗@ดTw]zH>fฤฝลฏฉแฦฝŽเํ1๗๛ย฿ฦkศป๎๙พ๚๊๕๕ญ๓ึ*ืY}‚wฎพ;Xž๖gเง% 6แ—ec<Œr[HะY๙‚฿ฐEEๅ  l0อด@ซjBฝจฐ=Ÿึก;^๐S๛กซ๐ด)8๒โ^0;ุ๑๚nSอœงลr„dฑ Œ Ÿี7t"QRฬฒฦ็ภ๏A฿.‡@_แgดส€ั๕แป97ี+C8้Ÿ๘จผึ^}ฯงฌ๊sึ7ถช™wจ| ึ฿—?ื:๐๗ 0žPnํ๏k n O=๑ํcšแ‹›4฿Rฑ[/ฆiฅฆษ^ŽBL ิL-๋—ฆ]< (%€.8 vhj๙|?แรใ.ผ๐Š๙๎#แฬ‘ำชเW_๘เs›ƒอ๏ญญใแีตSY]ด๐ฑ^Nณ•.ม0h่@ฯb๛IฺดุC๘1 9๏ึฃี_ด]{=ลณใผ˜ฝ4`Y๗Ÿ๘”ผ>>๗‘๛ใY^๓ใ˜Uh)Œ๔;๊_–โบW—'S๗H๖‹๓p1๑Z’t๕ว˜ณู ]Œ,X๚+@'–<ๆ้ฅต฿Hื v?๚` jP},@*์@฿ธ˜@10…ุ^ฬ๘ๅkqู-๏}ลๆฉฦล^ษiWภ ๚•฿๛žง๘uฺ๊z8์=มgŠˆใ๒sE 4m๐ํ๔%1ศื‚›ลเ๏#H>๎,>ƒƒย๚ฟ…ฐขpํก็็‡C๖ฎบฝ`n|๘ๅ]๓่9ช ญๅ'2,?Dด฿ฅ`*_>ฑ"CX’—ๅ“ฑฎ8F๑ฅp๖vQ!Œิ qlShฝ<0โ „\DฅะHw@Ch[๓ตทZœ๛ะj7แž(€(G^๘ˆo\เW๋อwUณ๖ฝuwเv‚๚‹ู„Pห$ฒ•Fb๕$t?‚พณ๖ฝ่ฌ>Kซฯ‡ sXฌ?๏บใ๋ฟ๓ใ฿_ฑฎๅ}W๕เ๊่กณฎ}๐๚๚Vg๙ฉ๓๙-๐ $V_๚ๅฅๅ0F๑WฆmBา˜ฏ€l‚{ษ2‹ศฎCqั‹€P=Iโฌ~L ฌ˜@0โ’ tJ`ฑ๖k›ฺป]yƒS{ชโ9๗หu‰_ ฟๆg๕ํ|ีด/ฐth?๊ธ๊g11ง…–˜}’#๘บ‰Iฯ=๘ฃี~>pเฐ4‡?ะ4žณฮ;๚ิฝyปt๏ป๒’ƒ๋๋_๘ฃณฮบ๖ปึืท1›T๐žเษ oHฆ๘Fไxา๏ง˜:*ยด฿ส/(+ุ—ัvลXk๏ฐ”ฌิ# ฎ__๗ฤ6Rหค @3ฤ Š @ ๊”ภ๖ฺ›พ๎แ๔อๅvp}”3AŽนจZฟลญ.ญ|๓ š-๎์ซs๛]“กqข"ีง8'ŸโHพ<ะGT ??v็ภkhยแMิ^ๆภ Ÿ๐=o{rฏ3‘O\}ัฦu[r๘ฌc฿ตฑฑ…ูฌAๅ!ขHฌพK/Iœ ๐หง”ไล(~ม5ศึญ๚ฤNEบ˜Aปd้ิต8”ญ)Mั~ห—ยjรŠ$ฎ€ฝv 26„+ H„๙|ŠƒใGำฟหp&หฃข0ƒ~๒'งš5'฿<ยU‹ณœฏ;Eะ๙๗NN$~}ฏ›„Dz œ•ˆ0ดžแ`ƒpเ/Cณ๑๊๋6ซฃ?qูjoœ}฿K๎:[๚ื:|ํ%ถ1›ี˜yn)B๘` วะ๐ว,๖*Œ ธ^ขRh‹\R%ภ[ A-K cq“Eข’ ก๚uHบรภ๚q„E]a{พโรw9๚dZbฎ/rฦ))G^๒„ƒ‡แฟ›f|‰๗แb๒แศีD.ด# ‰7ผgZ—Aพ„C ๓fีอ@aK ็.ฌฟyพฝqี“.{๑ัน{๘=๎๓าร‡Ž?fใภ&ึึjTพ‹ctภ๗ t‘@Yzๅft๔4Œ๔1ห”G๑9c๛d‚ล4–] ้๓CญkเCžลฒ‘, ˆt" ย|1รๆ๖๚ฯ฿่๎ฏ•kีg–œั @Ÿ๋ณ_๖๔[UavWv๕]จ ท๘6ไ๘๋ˆ๘l{rrt@„ภLฮแณ}ยฑ0มะ‡๕๗~๊ร>rไศ‘ี฿ฟ๑พฟผq๐ุฯ8ธ‰ฺูณ*๔ิ_๛NZu‹H๑้$€ชWึY~=5wฬ’k€ฏขไฒd +H{๚m†ู˜๔ H ะญ7Aฬเn๎€ว๖ขโ๙ภœs๗Wฟ๚Dฯ™"ื'` 3ำๅ—า็ๆg-ฺก†ฐ6ƒoฺjฎ›}๕๓?tlง฿า[&๛ฆ‹ใม๕c/=pp“ึืจf•๐*โ~เ’ „๎Aภnำ\.Kำ๕๔วVว2)ษ๒—€ฏ๓ีถ9>`ง๔?{๒#.€Rะ๔_(9H(h`๔ 4„P{lฯื6ฏูžoํ_๏f:rฝWง[๖๎{๏๓w8t|c}}ัBKฉ,š>?IPgJ#zาๅส๋dฌ€ะ•,EP ๚Y“€๔…ศm1˜ PใJ, .u€ก2%ำ$@๗š1ญ๙๚ฤ์7ฝหk>{ฺโ.‰;๙*๖|่M_ญีG76ถ6fU|ว๐ิ_Z}ฤฆK+ชYY_๖;a๚ไฑ.^Hย6๋<—–“ค๒™๒๒ฌŽฃL’NbฟUฎ!ข1ืขไaˆืฤร๛6๎ไ}ƒตj~‹ตะๅ]บvba๏eR+สัฃ—๚:4x`c๛ีฌAUu”ฟ~?Vก๛7t๗E‘ึข๎ Q™^ษ0Wศ+ ส}ะสE.ูaุPเ—๊^ใ๕คb๘Eภ๗ฟธฑ2ื*ชชฑ^m฿็+ว๘7wแ!๎‰L `Eนํ๚ฟ์ฦ๚๖ืf5fUƒสuŸู& A?ˆ>Qด_Šึ|ง:‚G*‘€NพฤSฒฌ๓2ฅ ึ7–ฎŽฃฟ*d]Crญึ=ัมFฅ”ญนษi ฝOณjตj๑c_y๏eแS:#dR+ศ่โ{ฎญฯammชjฤ็ถปOกcx3Qk0Œฦ•ศ˜"่ฌธœ,\RŠ |I^rmf „%E`นฌN•%ฃฌfโ—ค๙}5ฌ:าๅ~ฌถใบŽีdl@ฐ฿~{ญZw‹฿๙ท๗<๊p=“I,‘ฯีฝธj๒ตู|6ซ๘ฮ๏๏?ล…N€า&—๐H-1ชZฑ”มŠt?2,ฟฆฯ†๕๏~หJ—ฌปaีณธม2ฺฏ|์{šมษงร’mqํ-3ฉ4หUณ”@˜๙๙นฎZผ’ฏพจฺq#C™ภ๙า—ช#kk‹o๕~?๗ใค่ำภŠ‘๖˜–ูษ+ห’อ๗่ษ<+HV๒มม ŒภVก4๚โต –Š [Hๅ&๏•ซ๎—ึ B๏$J ŽJm0๓5fn~Ÿ/ธ้ฯž่ำ ™ภˆํ.บำZU๘ฺฌFๅC๗๕]t]ิ+ถM _ส•Œ`฿Pฯ#๕งยถณ"้ฃฑiฑญIAhเ[้…๓1ห@l/ปŸฅx€ม,คŸ_vZp ็ชj™Ÿศปญp๗V&P#GเฌyมZต˜Uพwฉ๕บ๛(”าL6็ปุจ่Fo๚d”‡Z,G ภฒh+ศซธ%€\๕๑ค็,๏ eภJ1ฌ Qฯ@3ท๕ถ๊p ึชลฌš๑ห>q๕X๑i๎ฉL   นห~`ญš฿ป๊f%zฯ=๐„ๅ—ิ฿คEไ—,๑˜Œะ๛l[ƒUŠdพช ืๅœ]ืX็a\oF ื+ำฌ{YRฝ๒ะh|โp#P๙k~~๛s6Ž<ฎ2)C๚฿}hอืฟ2›uเwก ๚น๐ฑห/๚Azส๊ซฦ>Šsํง‹tซgŸืฆ๕ี ,ะr f3ข/”Aฑ๗`์’ฺ›๙๚/๏ —า€ฅ.Rroิิ๊l#>3ส:๔ฑธG *ฟ€w๓Ÿ๚๚พoร.“0ไf7š?}ญZฒ๒mิ฿eิ?eษ<ะWฝ%pPถz]V@ฒb๕ึิ,f]ฮ! ฦํงฮ5๙Yืe5๘y(3,W ฤšbPZ{u›{Fะ?žศ•o0ซๆณ/^ยG/๕;}ฺงS& ไGxำตjŒพฟ฿ล>$๚๛ฃ…/ฌgJ`ฬาŽh.ค้ฏAซ%ศ๔13%˜เ_น6}ผั>{‘ง{>FPบ๊ฝœm่>งUฑ‘&?อึ+ŠศBื+ฐ}ฯ/|mxฮ`™€’ูฦฑŸZซ๊sZ๋฿Fw=กŸิๆะั่—ภฅeoื‘๛(‘ึฅ(๒ณ"โ‰๕”ภW็ฝ*xKŠ`T‰๗EGฅ `ุบN}"_งlI้าผง๚ธ…{.oeฏSW`Vีฯืซ/=gจL @ศ๘ปพึอ๊'WUdl2€ไลV @&*ค/ณ€Rdฤ[๓ำข ๊๊Aๆr*mบฬn8ฦR7‚์ฒŠึ)ำกผœ๙7ฉพ˜:ผ“้๗ซk]tFn=ฮP™€5ฟŒ๕ช>่]G‰“9๒#ฅค้>ูnฌy๖ฆหะIฉ‡j๔kฺ็‹rE&Rš4]๗ํซs๋ํD}K@ฯ๚<4ต/Xฌ ศฌ3‹6าฒ}ุุW–aปn-ZgGw {;•ฃ€ส/Pน๙}—ki™@'s๔ข๓ฏŸ0PN|8์ทลนeล#ิ้ซˆU^SfŒt]‘ ิพ’ ˆbภจ?‹h๐#ญ'Sl๒4S1,z‘๕irA@ใžษใ—โู}๎ข‚‰'=่ใฒs\€w *7w~V69rฦแํŒ;กฝ’C‡๘ฉkU}PŽ๕๗Nt!eƒๅWด~Œโ—˜@ ๘Y=0"ใ–Ooไ๋Ys™eVว*YtํR$–ศ๘1ปสOส˜"‹๖KOjGย๐–!%๒› }7โฐtฤจฺaย฿ฏรฃq†ษค|์ชžํ|๓ฤvฤ฿`#ถใxุ๏Ÿ๚@nร ภ2๐+Jl2ˆBูา˜๗Rฐ-–ษh๚’%ฤนikž—T9ร]ษหI”aญใ็ภกสeเg•CypูeฐžG ๑X‡มAžT~ŽชZ๒g๊า+^๑i‘Iุฦๆใf~qฎ๏h›#tรkฝาzXิ ่i๐หuำ๗ทค`I-ขฏ[าw eดฏฎำM6 สkฐ'#๎ฬ‰2!‰•_f๙W‰ฌD-Dบrˆxpึ?g์{ภG/๕ี?:๘f๛‰‘ƒ๏ฏจึ ฿€3๖Œฏ'ม0ˆ™ฒสพb)หgŠHญดข๖ฺงg ํ˜JH—<ฒ=คrซ\Œˆัžี`ศช E>H%„X ยฟ~๊/พิzJe฿+€๘าƒชชฆึ๚o๙‰V`N] w]๚{‹XรoQX7€Ž”—‡$]m›๓่ญtฐSฐส่sฒสษใI /9ล—นq[,wa'ภทvxd @ฑGภ5๐~๛ฦย๖ูaํงL๖ฝจช๚‰ั๚วnฟแฝ~@n—ฝ ฃ๔[ฅ ฌ,คฮฺŸG ^้—›@Y7ๅ ๎๗CTPCี5ฦ%€x}ํ฿๋‹s[ๆ่<๋ ๆษeข"p ื+ไๆO๛๘}sœฒฏภt[y฿< N๕~T_“๊Kw`ลwฏCพžน๚G9ภeฤ?ณพZ ฆŸ>ฆ 2ฟด/าz๛ ”[[Y‰|ต7ไบ>ฆ*฿งมฤ]0Ž)YŒ< ๚คkฐMงN xื`ๆ็‡ฬถŸq"mvทe_+€ ฿pๅk/g๛ S~ ไจ๏^tตƒ๔๑-เ๊ทเJฅŒ+—z์…}บXjท J(Yjฒw‘ณg•….ฯฦ0]•dn@ ๐ฅ๓แ\ฬx_-ท€ํ็‘}ฐตUฮ , r๓'~โM{?Dx฿*fPๅยyืคƒ~บทฤ)ฟYเOำ๗าtบ_ขHทณ—_Œฐ์ฆXิสzkŠฏ(3€‘ง@šื๐ำM`บ>s}ไฬ๋‹็ย…ใว<ษ˜Fส๊u ๊\ื`ๆๆ‡žU๖X๖ญ๘๐U฿รป๚›๛ศฟ๖ป†ฉพŠ๎“ถ๎า๐HŠ h—@wŸeสFYไmM3†ฬ-ˆee๔^ิSŠซŽWdŠhŸb ๚ห>:ะ˜mŽiญ'็*™€A๗-wฦ๙A์ณฒDEะฑbxjเ] ‡๚‰Ÿฝ๚ั7ูImป-๛Vฌ๙pY%F๕ณdฟ๖omๅ๛๏E~)ยฏ๋เzŠ ขPถล , ฏJ๓ม#e ขPฆบฌ{ฤˆ…d>ไบ<F(ฤฌธ@ฦด,a^ิํœ{(๛R0ƒˆย#ผk2฿?ฟคYฟฟค… พf หพ€ฃ฿‹ˆผ๘3ม˜๕N๖/๘าะuTฝไ๓g:@ไผ D‹F[็ิ%ƒqM%—!QJYŒ-ใฑ—Iฏ#†ฒิ+•ซแช๚)ป๊ฮ^^ูฉ‘}ฉ>๖๏ผ“Ÿ5฿เปi›ว<๕?iเ'ผKำhฬ+4‚b/€ะด]ฦ:aฃํQ๚ธิี•Q€iตูฏฮวpbf/ิ1-šฏืล2ปVฅX ห•ุ๋ชG uบšŸทพ~ 'šON๖ฅXใๆ’Š‚šํ' l๙W๎YA๋ำXภญ7โ"x)(จ_›ค๛มPบ}.s —าŠvYšส๎ห:ญrP๛X๙โฌxC‰ๆ'ึฟ“๕Qงุก๏ๅ๊h•ัฌ\5฿<๕cW=pdฺ๔‰สพTDแqไ_:๊}฿:ฎ฿๘FF•oŽ๙_ี ค ฐ#:p{zmvัยฟ’ีต 9Pณฎ#uŽ๙๘–ข๊ืƒ:7qํลx†F9ซ}ตาBY๔ฏ‘๊&œu=•พๅฺฺมG-oนป/๛N|nๅšปนŒพ0่GRอ–ฝ๏`ํไเืฝ‘mซ๋p๔งY)ภล<‚ }ถบ๘Jึ< เ้ฒŠ)ซN๑03eฃnk=žฑW4๘ึurI…๔nฝ›,Y@๋ ,ฐF๓Ÿ`^Y}ูw `ญฺพฏ๗Mี๚a๒?ย๗—V_ำzณ๏฿Š ฌ?ไe–สโ๋ž}uฺ=Lถ  ฝNRึ7s $่ ๅฌŸิ\ผคœf'(œT96ฎ))#า สe๗ฤบนโY๔ฟdƒะ2๊b/๎๘้ท]๚;jฬป ๛P„๏๐๒rฦ_๖š/ฃ@ฒ—่_iŽ@ท—„แdส`‰ŸŸนโื็VฒHeๆ฿๓’ด’ป ้ปฅ บ•”EษMะ`ืพฤ๊+6#๏2YV&ฦbAp ๐~ดถ็“•}งอ}๚O๊u_ ่Q;$พ}ฺ›SWŒ๚วผฑ/ๆHะ&ƒxฌ กv 6ะF(‚>ฏ๛“Xj ๕Ÿ%/Y}ฐBีฃkะ๗’"ษ\ ุ๕[?‰่ าฅ(0"ถ๛็l คG€(~eบig V‹฿ท฿mOขy๏X๖•๘ฤห.ฺ๐>|›#ฮ~WœใoŒ๊ฃBzั๚—xf*าฏฉ๙ุป๔ณwˆ:-I@ำ%dTX)จโถ—y(”Yส–”้ฯ[ํำ_Ÿf๒uโขฒuสำ“Jฒ eˆ๏˜ˆ/ i0ฃน[ฃํ'ฏฐ๓ฎษพR๕ืฏ_เ)p}&R ๓ฅEoง"๊—†๗–ฌศ—uฌฎ@หะ“‚,j:4๖)Zพ๎ฯส?ึQดiƒก$–)ไ๕ย8ิตk%}ขœXค@'Q—๎YQห’$DPณ€vpรโ1z๕ฅ‡Oชก๏@๖•๐\฿YำRฺož W€J>’฿/๕ ุม>ฒeึเ5ล5hqF๗ฑ"€เ๛ci๗bฤสk…Q ฏหt ”ษ๎๛๗B฿Gฝั?›ฑ฿ๅ๕ม@กไๆ7ฺฌ๋ำ๖๒ะ}ฅ*.H>๎‘ผํ'H€W์ุ!่yงŠ ๋ี[(๘}Lย>๐–ข่ˆ ๛•ฌบโำบN่ภ“uๅฯg๗E<ƒ•CY๊‚ƒ1เ\ O๕Oธ‘๏P๖•p๛ว`๙[–ฏ-ปฆ๖:&P๒๕OPdoึ‘iโK<–าHกf3@ฺ่‹T=–S4฿ ๖i๊]R…๔ธ๓าXสu& ถŽ%ฎู>็ฅbฆจลฝ–๋Zโ†๚;ะนž8š฿ๅพ๙w?๖ฝcฉNวAฮ!n“N๘‘พ?๊S?s่/P|ุฝ๐HูโJ”=๎Oฑ”วO๖9'็$ซˆ"สืฉฉ๓้๗ีดWm—XqŸวF!kข2โณ๛ถJšชฬ€ณฑŸ๗œ็˜ด๗7 v“„*W?ภ{qŠe฿0€]๕ภuษึŸ,PŸจ•วjiลhy2X" –ๅIlฤ4k]ฒฑฬรโBฅs)=ำ๒๓GฌRืyบผ ‰+ŽๅฎฬคผL๛Juม pwผซฟ๏soกC8ลฒo€_฿>฿ึ$๐\‰oI๓K๏D!ฝ๔I๐าWqJn€I5ตซ ƒ–RเดฝY Zบ?ฆฉ*S3Vช่-‹/d7ฤชS_็ื฿gY๎Bแ$๗‰หykะๅ4๊—-๘# 0œซQนล9ว๙ุ#–ท์““ฃศ]โ“xำฏฐ™๓ธ์Œ@Y๓%ฬกocŠคหOrม ๒}%xdใ6™Aษ฿๑ั—)…’Eืภีื’•มpฎ2ะหs“e๚็ซสsiฉŽํ ‹„V @v ึ๐˜?งX๖˜Utำถป‡>l?พd๑w่ต๒๊?ำ@ภT8™E’ฐEฅŸkไ–1ƒ•ป ๅ9ภฮ+–—ว@พ”Q&q|]Fก|%ชฏ๗-ิY@dhg zW_๔๚จ[โสพQกแ๓ฟ$ฐฏc'๚Cพฎ?ส9 ๎[P์.Lฌฝ5ฉฅ๑“’€ภ{—งƒ^f/€่๋1ฌผ ๎% /น @ิญฏืขšQˆzฒ^ ค็.๋S‡/*‚สŒnPŽxท๐.?ฅำ„๗pฮMพุำธ%ภ–•b€;๛๖Œตk ถ—ัD JXŒFdสbป์ฌฎU(2%˜สภขฆฒะส้๙#ฏfX็t]^ใ่=ฯ+mŸขˆvd งๆ๛Wx',๛H๘C, ฝAG}}`วŠมฒฺผฌ>ไ๛h๋žํ ,9X๒๑M+ฅ่ซ ๚1Kmน;ืr L๚R 1{aUฤธ~}ฝ๚พภ8ไจ+ ุƒYˆ๚<ฉˆธ&์฿๖ั+๙ญ8Eฒo ยJ%0Vถด,ิYทUถ“ค‘,๒๔Eัๅต5‡ุึด]บ”o*…eิ_—U็RŠCdฝ๐2…ผย>:ฦ•—(อไณ"จฬaฝwMฃ@hเPำฬ7—โษพQD่น&€ฐ-}†๊+ถW˜ี–Šภส+”ืวะ3น TL๓ ช›๙ฝ2/ฎ€อึ‘ฆg:N๋อทช;˜Š@[`SQศ๓0ฌvๆ(Eกื“ฅR”cB‚`xeัโ‘8Eฒo3ญ ฐ Eวพุ›h๒%.€์โ๓ื?0ช ’2(ื/A_ผ10ฃA9h€ำ/นล8€*ว ค+E๔นศ"๔r“ิพฦฝЉ’็žล4+P็)Ÿb๗!'ไiqว_๙ศoฦ)}ฃ"ฌA Œw‘ษ๕R0ฏ`ฉ—M๛ๅล‘ล €ขbศพ๔+๖ษ^ฎู˜๏šะ™ึญdŒ 6๙_6้'ภฟู_ฏ•ฬ*ฎืฅฏลR ซฦธฐ]ga฿ไ8้ษ๔ฎ@œ D5อโœู7 €‰Bwmใ~$บn•หFเ้2า.Y F•ฑ_ษ๚›Š นxฃมษศC+_[๚b,@g)k€๊˜2๕้จ<ินขp๎Eล`งฯJ%)[8$‹ฬ%๔ฟšbT`@ <๊‡ฌTมe฿(o๗!@Ž—1ภa„Žหt•oอี€ดาญบวX„>๕0๔L tŒBšฅJI^ไะ–ธฆ Œ‚:c(\ืK0dึบค๘ไL…`jแdั?–ฑ€ ๆ:z้yุeู? €iซ‡Gาๆ @ต"๋M๋บคwก๘+ลPจ'žงq,.คY็ld‹4)ฐมd^‰ขK๗ศมhนY๐ฎไ”‡ลŠJฦ๓Iำ็ืง—”PุO‘‘ จh1[X์๚[ƒ๗€รฑ$˜อฟฒb—\Rถฤ$€2KbX๙d T:าฒ๚ฺคภ’rR"ม VNห˜y%&`ํำUถิ ๋œ„ฑญ๗ี ห*#ŠสกtŒำPT&†Bˆ=@"บŒ๚?์ !ญ$๛็}ฎw#๚‡ง)4,Šะฅ—ภ^r ;พ<ซบฬBโผ๕zท/์Zช+n“Vz]Riืป โa›ฤน‘ษsPy่๎ต‰๕d*๓*Š@[เBู1?=S Vฝj]bา๛˜ฯexm €ศล—…{fัhˆsGฒo‘P&]ห—PXๅ^ึu8VKาฌ}ี๑ญ†š็Šฒ•2Y€ถ๐†{`Rxไy๚–ซ _Onฦ nBZใุู๕+ๅTบr#๛Hท!ษv ‘ื๗ ัโ๋๎Oฒซฏ ฿7 ฐ๛W›CดZมp/‰๚gหๅN๋5๒3v ญฟ8ถ๚˜‹S:-ฒ๊ึ, ™Tข4ล=ท‚Z๓„Mpz‰ษ}๚nZmฃนญ-ผฯสณ”‰ฎžUžvด2PทpB฿%ธ๘~>Š]’}รฆ/0;1@IขฑI=ฤ‘ž‚"pKฝบว`I—bชžx^ู[‚€ผ[PœS฿๘eล ฺVnfญ‘ฎgyl์งŽ5๊›‹}‹= ฦว ศzก๒Œ{bตu๚ฅแ/”โ่ภ‡Dอฐ‹ฒo€็ๆ3œ€E5hiฌๅCYูืK ภ4k๚Vฃผl`ฅ๗ื—\™OzGJ'˜๋ +ˆ>?ฉz8=y{Hd4าฦ…ํ1+n œQฎ#3ุRแ(…6zš–๛!žiw‰ข„pŸx7ฑ ฒoภMq‹mqlx๒›Oูส ๆํcc}๔1}7~(,ญ๘‚๎ัฐบ0ปยI›7š›ถR๋.-+ \JF ภh๙์š ่eฌ'9–fฦOำ|mัณ๋U๛eฑ นฮให‚PW†ธฆ๘ถ O๕ืŸื=โุ%ู7 €.~gอLŸฮุI‚%bZSต]๊ฃ_ฅ๛pl&฿*๏`ฃ|rญฅ.K๋š4ฐฌ{'- ๐Jะ่บy‰๊k“j ญ‡;6€#ว# รJJศLิ pTSM๕=ฐKฒo@wนg+(eT ”"์€ l  LZฒฟ>žผ•—]—ฑ๋:0œำชฟ>qฺ๐กหP!_Qe –เ†ฅทXรุ ฃ"ˆG๖MXY็{{วใ”‚ษZฦ๐ยPB@@ฝk฿ ุW €เ> ฆ.ฌ‚”ฏ$ฒ!u๊๔Pฅ฿,t๑ษบ-–ณ๔ำa+t j:.’หม1ƒZ›€%& *6ป๗DYบ>}๎–› มnิ—)=ฑญำ“ฅบ๊สชฌ ‰ˆ›ปb—d฿€ู#ร 74Cแภ๐กKุO€%+ฒ”‹ซซe=0@฿ๅgืฎใั=(ฑตZƒ†โpX\—$V~1Y\–fญหบูภขPœ%>zŠPล%`( g…˜wuPงคcท !ฤwญA๛Šย?p๑-ปP Cฃา>]QJVบi\*W:WE๛ว"ฌiนฎงฟฦ%LฦxำS}‹fหc/กอiPวีt ็%ฅศ๔น~ึ๕iฆ“ฌหฅผ๏F{ฒ˜U–ษ๐จo๖แ+.ฝ9vA๖ บ๚&฿0joNˆ้•x`P้ษฬB ๙}yฮทฌfัODข•ื'ฏฯชYืIYRถ!Cฦvฯโ}U‰ึฝะ๕hKŸ\6ฑq<pฃมฉPืa5(”|ฉ\ถ๊ ด\วˆ-x๛v>‡“”}ลฮฝ๘พาภ}2า…1‚Šฒ˜V$สX0ฐฤฦข๛+ฤฒใวbไวdภŽ‹pžg]o†ƒ1˜lJV_3จ๚xษoIYMแว†+[็ี฿Uถh5ฯ’—d64 pจ/Xฺ.W}ลภ1ฝ—™พ)˜%k.Y€lx$สg์@Šฎ[๚๗œฆiซ<บ฿ึ`Y} ๐:นT็-;ฎ.*ฌ~โkf ๊ฮ็—RฒไET๑’_ก^RŽญ๓`ฉุifสขษ ฦฤฑ๙ต,€นนอฒงถŠ์+ะ^ฑ/ณ˜wฏ8tKำ ภ }€ํ๏ำาO4๒oฑ `y<`Œ `จƒญ%สุ~ฝ…/ห๏ถ“1ฌtU้o”hf ฏE[๙B)ต๓ผํำLvี๗จ;f๛ๅ ฐ๏@ร๔WฬิดŠ"-ฝซ`}ณไRฐฎ+l}าด๘…บF>fญผธ๋.\!N1ฆ%X zL$ฑnm6๔WnHf`๑e6ถ:–Yy]#/วศกˆ^ืเ๛f: ึaป!|๒‡ณ\๖๘๒โฺ}sทqจ๖– uƒŒ–ฝ6ž‹Aอw–blๅR:ห@<ฝ‹9"ฑp์ค˜:Vi™[๗ื้}k,๓ ้*?ฑลŒ๓ศ‰z^E@ƒ฿>ญdQ|ฦŠ-1๘๋wฃ+p฿นทพ๘[!ธลrํJ~ŒB@๙๖S!o…มAKฟ$๗)ฅ[๎ƒ๊:,ญ@ >Žบ7 ]Fก ็๗:s bžEK.0๎:๊ (ิ)ฯU า:^ผฝด๐ฆษ™ฤ:EZขcธภ‹ณ?|ลฅ‡p’ฒ๏p๐๏ ์„๕PำJ““ฅIe+iP/wXR.žKฒDn}Kภeำ„อ๔s0ถ6m,0๊็ด๒`ภ๖่H?.”ีuˆuื["#Fู>™6โLฬ๊(’;พ8~3œค์Oz/ปjดzNะ•ŠŸexYm—๛d๙ฺ‚ถb@9ญ๔•แฌ^qฎ๚บFส๚™๗ €ฦพฆb(-วภ?pใFŒ“”}ื ุ Wo\5žŸ=ิพ+่Xึ-%ฉnท]ยB฿Tื\ึลc๔ี—ฦX็ฑRนุฐWํ”ขป๚Vu ๗'ฉ‘)Gd&ฬ‹UYkป qx$๚zฟย9่kแ‘ua`oB‘‰ถ8#‰) †ฆ^€•C๗}ฟ4์ฆศ ศีฒคง1Y‚kค2จc]f•nพe๕Kย(๘UC#,ข$&…g‡\ุวชฃ@แวส๗u๓’บŒฦUY@ทS๒všืน g8Iูฟ ณ{w/r!} ๑๎าคฉษฆฌ(i…`(—d0็ๅ’mคe๛]ฌ|yeสษ2ษห˜€1`๘ดณฌไ~๓ศrŒศดฑผnนœWบ&{ัnƒฑ.‹ฒ–[ฤบ๗X๖yฤ'mภ๗-วี๋;Nๆิ'ํm v‘’ด›1?*O–Y%€Wศ—้ึืLw้K™ฎNัต๎หาูศ+ฑ€‘ไ…Ž–KŒีฉo\ร*ึ>Q€๐_8iูื `ใ;^๑ภณ๗# ฒV๚ฤ“AJฑ,ธ”Uฉบ5xgี€RBู•ล|Vy™b3‹}G้;0n‚ >}›อ๑HŸ—นm) ๙lq%p"ื4l็€gใ'š\ศหFฝB3•}ญ  ๎5ร  ฃLj|)[ฬ๖‘+€t•1ฃ]‚+tZ็4๚=’BะวนEฦaAญ•2สZn*ฌฺ2ห”Bf‹Jก๛6ำ๑&NR๖ฝ@ุ8ฺ฿d]บoฆh‰TC-)Tซ(น=ึื”ญฒ.IK‘X้(ƒ~U‹/ฏ?ฬNY(ฏVd#วฦฒs‘ํDค…qๅa๔จ `คw๛„ฎรIสพW๏๗šฯ0;”ฺ]*ค@ŠEUฌo?ZV™ฦ^Xrฉ~u๎ูuสa๓YชNฤUฟ1 ˜ย(3เ‘6ฐCิฅi}ท~ŸRL U „€š58Iู๗ šฺs็$_ ฒX@lT๑I ูษJb)–u™ญ ฐ@ฏ๓—คeŽง\ฐ—˜>*๕+2CN๐ฅ4#ฅcPบV=ฃฑo๘ฑคึ> UaHฑYฮ›ฏเ$eRฎ ี๋ฯพฺa4NQยX,y‡Gณึึบภ“yห€‘€ิฃ๋& ลกบข‚•—+R๛๖ฌฌ84๊‰t^๛2š฿ฏ๓๐^)t a(ยยำwุเ2™€›]|ลuuใŽ"8qšฐ่’Š >M y๒ซ่Pz9๎'ซ(Uzิผl]œปพะ0ฏL๛—( }=Ku+ฝrคข๒ล๓วๅ๓€ฒˆ>๔zขH0๊FฤธŸ]กตสค:išต฿ ม.@์c5Gy) IYKŠ฿%XฆDนฅ=$}Oภ:ื‘uญฌฯŽ%๎€พ':๖ ฮg%WภธวK้น<}ถห%/7ลศ>๙q-Y฿T?–‹iกง„่บฟz๛ต8I™@'/zํ๛๙คcH<\๙ไโ^!mˆ’)X.AฒU‚+ฒ‚ั7q=:๘W๊๊ณzL%ไ€–๚XЁŒ "*ึ‰ฤ?Gฒ์ึ•ฅTะฏตŒ2-ฺะEC?๐Mpจƒปj7๘ค–ศ์ฺcฟ‚—ž$aKํt๋ฃภ7$กูQJ`ถ๒Œ2Lๅ}K.„•.A•œ›ก ’r” tฐ@ซ˜Eฦ Dฺ( วสบ{นN€€–2K#๖I0H‹/}Cบถ๘B4-Xิห[อw-L `ฉะ%WGS= ์นzrˆ์ ศ Aฌหฤ‚ฮม2+฿--?},ขฟJLกTgฦฤu˜๑ฉ ”‹PฺO*‚lปฤฦ,Eะญหy;่iพ๒็SŸRšŸEEฤzฬ B1ฤเ_:๐'UMh้/;rล|7๗คV/lW/แเลn<:ื!!ZYษ๚๖XV]ฆฏ่gห่•๋1 vค็ณ4Eฐ\†e~้}์ฦ @ บoญ๋‘}2จcฺ๒wวf๔ ๅ๒๎>'ภ๏ะtฟEใP/f/?ํzR;๘ะหนฎ>j*ฤJ)E€˜o™Spdbฮ๒ั—v—‰็Tุ?SLe ๋ะ์@w#jเg๋@ฆ@ฬ?ฐ‘?CฉคSx%C์Q$@™ฎฦ(เงพฟK\&D๊Om๐ฏฎaฟ๒ึ๗žŠ6=)€ํ๒Ep๎็Z7 *ู#ี— ˆ ืหn=YสF(๘pFศญ๙ซฟ4๊/ม^ ๑ธK๑ ถธ?Y|@ไื%#@มส+6!ต>ชo๕หˆฟHb”ฆq5จฏ๚pg๑.ภ–๚ืGอt๙˜‰8™ภลว+_ฯก๚ซtp”ตaไ๎@Wล d,AฏฏิฺทๅำCCw&๕ heP๒๕ีy,‹เยrV`†ษ8š๎#U ชปฯV ศ•€๖ม>nญ<ณห(ดง๚๖เo‹ฺ_๛•ูแWžช๖<)€ ˜SŽ๓ฑบA"ต๚&่๘Jษ'เทƒ฿JS้™ี/ิc๖ ่๔1& ฎ!cศ•€4™ฟR@gๅ9ต๒}šE๏นuืJ eMŠ/ำ\‹๛ƒ฿฿4ฎ๋๗ฏx์‘?๚สฉjฯ“8ก๛_†P]1€_|Q(›Tขปีv|Iฝ“#ซ ะฤvฒฎK#้"ew@+•o๙๎ภˆ ใžh`ๅiRbณK@/๛๘u7่ำmgษL[6ฦ_ฆ ั฿Fƒร|Q5‹เžw*๒4๘ลฑ๛in%D8ะถ2\ท\ๆญ๚]zแRAŒิG…๔Xy้|ฌt™&ONจTV2–‘(9Jห2‰2๊ฝR”๛ะฐฬ๊Q—:Yถ']ํ1ธซฏีป6xX็ถ์0…—บS`0†ดaภ็ฐ๏0WคณJื/„†E/@฿๗๏ฑO.๛ท|งP&p‚Bฟ“๘gA~G ๙ž€๊ฬz,wภ สuหท‡Xํ•ฉฒาบjฟ฿๊๎ณhฦTดฅง๚PQ%ฟˆอD฿๊๎K~d–? ˜Y๛|=กVšะวฺ`hฺ฿ลยshฏŸ๊v<)€“/,š_Gใ?™Š ๓GY)‘Y๚PE”„2อฑ‘^R YะŠๅ๙vRNฅeqคKถ–(€Jืeา™|้Œ= n้@ะxฑoฟ…บ่?็J F๚“A>ะ/๚[%Pื๓Pฝ๕{Ÿuี:ีmxR'!็?เวฬ~่,Fจmตฐ%ฅ^Rฤrป;ฒž œXRnŒศ๓I˜ [ช๓2๗ตา;ภW;D   ช.?sฤ_๑#:kฎ•AแืF๚}o๙Cg๕›ฆ-sใ้tดa:๙*& ๖ธ7Rี<ž8€c€Hผƒิฏหˆฏ๔}ต7YูฌหญX”Lƒี2I7สX๙ฐ๊ถ๒ล9%lGŸฏq,ˆsKส้t•/าธKk๖6=๚๋C:ภบS’i<ฤ TZ‰/นฮฒ/qฬIAFิณว #†}๔}Gh*4uีŽ๚kๅxฮvๅณ_ซศคvM้น{[%G สB€ภ„5Œ้ [่เZ<ถ๒ท”)$>tฉWvถ๖Lเ๗็dีบฌoe๐ำ่v:บoz,Œ๑kภร๚%Qง{ไภ—ณ๛šุฯ๏‡>P๕พผฉพxlqึw:[ํคvI่โwึTฏ'4~ป๏์_6Bใ^โƒkI”฿R ด}บุ_ๆeรƒ๕9(+nu?f๋โ๔zvM@(nณTrŒฆRk.‡ŽG๙ีP฿%/๔เFŽ๚ฃ$า฿๖๗{4อEป™วผเพt:ํคvQ่ปž๗ี3๓ษB๊์ฦฏ)จ+0ยฌ๎บ„-ฆ%.) mๅกภญ•Žiํm]‰%-aู„ž๎†ˆ>D>๚ั)ๅGJ๓E”?eฮด๘#ˆร|]2ธ‡%๐C…&T=จ[๋ํ_zฺ์้>เ ]๘๊#๘๓พ.tฝˆปU.ัะCP์l+ิ:[JD๘$ห€X/ฒ‘<ฝn•….Sจ:‡ฒู>2/๎ฃ๔1โŸE1D๚ป๚tC-“z๕F๖ๅ๛่_E›>๐็ะ0 cป่ข๑ุjๆ"๘ป|๏๓_งปฝN เฟ)ท…ใ๗ร‡รƒ่q ๘ๅ ๊ดC฿ @ชG@๔`ฐ”Y…ไQ [Šฃ ŠภQ>(์—(…BZ_พํๆใn{5เ๋.ฝ.ก;NศAบ โลHถ฿)๐ใx†#๘=๊ฦaxl๗ŒG:W ค์FธHืณ!บ้ม’ 4ีฦญฟ‘๎?~ด|‘JืiP๕Žฑำ=ฌ๖๐]พaเOดิm๑eƒ| ๖Sึพ/ฃ)ฟXฦo๐0ะ'‰Vฟeqะรขฉฐ๘/m-๘.—]ŠO๏U;"!€ูนACw่›เฉ Dm‹ัฎ~ฆ‹วI ว,•fZUใ`œฬ๚3L‘qฬ_!_ฒส@๏_$ืป:jปu ท๚ฑž~_G้c้ Ÿtึž9๘G‚*›Ž๘ฃ๗J e 3๐ญจ=ฯนze—žฟิส&ูEแท—ปข ฿€ m ภq `eเEL0€ฑ”4<Œล`X_‘f2‚Bฐณ|X๙+)ฒ`Oืsหฮ๘ฺสซ2r$Ÿbร๔^e๕…Bˆ F๛ล๙ƒฯ฿ฐ๏}yp˜7Y{ั๏=cฏ็8ลB๘ญ๗ีSัx T" ฿)(ว ฌ๒+QnˆtีS ๓bพ•ž_…V:7๋˜cKXy๙/้ึ๋gํ!‰Hฟp\ค/ํฐ&๖ฐ๎ๆ‹>ะวชซOv ๖ณšิโ‡P๕ฟ:TX4ณ?๛i๎ปำ';ษ)~๋3^Œช๙‘6ภู่@d ะ,(็€tษธ},@Y์,ุ€ล:ฬ๔16 ื-6Zp0+ซ—ษบ ึ?y‘G<|’>ะ๒ณŒ๚ทJ ๘๗~ฟog๚ฑร"8l7ใhpฯK.ฟ‹{&)p๚คูz่เํ€ๆพp่฿ M~ท.>H๙๙%acฝไ?ฏdฉ๘@i?๋\ฌ๒บnŒ”)ฌs7๑&ฦ ธ๓๓eœ!–aSo๘ัo๋ผ‹B< Qรฌ>Žภึpท:ภํv<๛๊f3{ุe—?Œ?01€ำ*ึŸผจz|๓ํ๘€ศBส› $j[?ฎ[เ_ๆ๛›๙'˜ใ8'ภาจ>„o€OูAอGž6๘8}ยHฤ็ภ:ฐวˆ๛๓จƒรv๐๕๖y๘๏ผ่อ{วZำ$งX๘ชŸน๏Fีœ› ๔ฎ€Vะ+BNfPส ๔Ÿ|cเฯ Ÿ} ~vXิฤ[ม?๙ก/y‹๗บZฯ$งS๘ช_ธช๐๘ฐั*€nœ %6\tw ฑ^ดz{F๒‘๔20Aพฬา๗ มฒ๚ๅี4Ÿe_0ยoX ๋-?ฺจ<8l7๔—ผ๘๙?ฝืํฮ’ฉ`„๔ฬwaแตkฺž/~r*ฑž๘Œฯ[๒ƒๆ%๛,ห‡จิจ้H@™>> hฉํ7”“^5ŠOอไl”Ÿ๕sขGภน่ทiqYูƒ9.=บ_g็ฎx๙7๙™ฝnsลถธื'ฐŸ…฿tไ๑จp9|M=@+& ‚พ’‘ํŒฏbๅz–๎cฌc,}5h~nๅeุฤ+บฯ™;@9/๙dˆ=ํ๏ญ?ZหเP3กfยV๘k๗ฏxึฑฝnk%™ภ ฟ้—žฯฯ†hw QูQ!ฐUกฑžQ์oveฐฏ ฐƒtไeฌภภ‹| ้%บoF๘ณ๕แณ฿๔ฬ๕GT„ํปถ›๐•อ…ฟ๓ร_โO๎u“Iœยo๚•‡o~ ฎก6ะ)„A$J€G*StฐAหZ pา ภฌc$ฯPฤH๛๗-€๏Œภงพ!๚ฯ ่G๙I๐งภ๏x0jl‡€c ?ๆaใ๗`ฏึ2™bg€ะƒ๎ูXธงฃž4U;bฐ™กp7j0จHฟ9ถB5หmu?–ษhDkNฦ๒อ<๕’ค้ฬ>เ!ฎ,ซํ๘2–๎eœํ‹<\BฤY~๚E๚MBไ ฆ๎%ขฤ`ภฎป9‚BC›Xธอ๗๏)๛ ็ฎถฝฝ>Iแ7ฦใเš—ภีU๋ ˆธ5m!’่2ิ%ษ#ด(XrY๎DฉพQ็าX@š—YaอMžล%q>†฿bฅˆ?sธ“p•ภ  TฃAšl…€c ๒ฐg๑•{žV‘‰œAB฿๓S/Eณ~๊ตอ–ฬะ3‚P , 8@nCค!ทฒ‰•/E๋G ื“} uše€|NA๚%Ÿก@ฮ๗G~œF๐“๙ +ะLaฐ๒๒ฃหั>˜่‚uฎ™ฏฟ ๒› ๊เ8‚DํๆŸœ๓ฺU{–V•IœaB—ฤะฌ}๊ต/ขYk•@TQฐ์2”/‘Šr€ค/)ฌgeFส&เ/tYš๙Tศo อ๚/ข^oZ&ฐน3ม$#p้/oรฐœภ หIJtž๕~๊@๘]ฬ„ี,บฉ฿ังจฟดฒหpI๕C๛sM~็kxฟ€๓ 8?~ีฮ/:Fฐ\rmฐ–ๆn—ฟฑืํf'2M>ร…Ž žษ๒ขฟD]\} จ๎~C`7อธwๆ1ะ~Ibฝ;HF™ฦ ฌฒฌสฃ๘‚ธS_—ช7้ฉฐงโฒ<ทb ‘Uข[ ‘a€ฉ1€ปู™L]ฯ 5 4Tw=1 ˆt#@๛ภ˜=เ๐ฝn/;•‰\O„๒ค?ƒŸ อฺ๋ัฌwL s ๚^‚๎ฤ‡ุฉet Œ^„ฤ]P๙ฐญsžŽ!%ถ!๗u™•ทฮง3ซท๓ยฐู,ชH๑ˆไฮ ๋ไฮ1œp‘๖วŸซAพ๎า›ึ-pก฿5 8•;๚ปฝn';•‰\„.๙‘/2๐ฝxๅ?ˆฆz6|}ใ–xัBI6เะฝ  ƒๅœb๔Œ`”ศ4J๗‘ึ8I—–?ๆงวำbv Qี-G/ํ๕Sทdืญ@ฤล๏๎Kg๙™Z`3โฐ์ฆฯwmฯg[&0UจNG=wK&p=˜๙„W ูธ=š๕ื 9ภh6€ฐ–ฌ‰ˆ„๘=ž@ว ,๋ฏC๗^s]„"{ˆ๏BO,๛P.ฮฆใ>=i๔๙่~๊‹H้ัาz'~ฝฅœg8ฯ๐>Z่๛w๋ฎiื]็ธ บะื•~์์wำ๐O{>N =Mr}~หพ กy.P฿ฎ .๊ ยœฦu ๙ัšB0ƒlึž๔ูญ4™Qฏฮoญพด๐ฆ/บCRV ้๎ด฿๘‰q0Xy9ฑJผ‰‰ฅ๕๏F]2šnปป€€YF@ร 3๊ภุชฎ[ฌ๓Eฟ๐ž[๎u{ุฉL เz.๔ˆว)ย?฿ a้h6พ8ฐ.Nฐฐ`=#๑‚~YBกGr6 ‚`p*]วœฐ๖ยฟG\ถ–,+๋ึ๘ผbk้[ซx๕‹ึ฿9t๑9ด์@ฐ…ถG€ป๚ฺx+u๕oj๋ใื;๋L1€„ะeGๆžหoxู๏cซ๚/ ๐TP}vยะฌb?๕:pษ9}฿๚Wแ&]๗tห> bลดVิ/uoC€8ะ}>‘cDษh9@Rˆi]ijฯ1๔‘. œ8‰#วh๗์v!!N๛‡=wC&pz๘cฟเ๘่KŸ‡zใipอS€๚Fฝ บS–"ฐ‚„ฑ+ัฅHL\Œธ2Cฝฒo^) ึ(ƒ‚vฉ0|SUtBด๙ิว˜จ4มuŠ + ฤหw/ˆ*ค~kซk๗iง๛ุ๋็"2)€ ะe๛€Ÿ็ฃG๓๙ใAแว@๕ญZEะ)ƒ~ฉ•€TrB!Hvฐ$6 -:†ชK~2bR t<bDŸภ๗๔ฟ-๔Zงcฺ7ญwœข กแ๛ aะ๐แ&0ผ<ิ4๛เ^?๗‘)ธ„ฏพบยg>๛Px"ะ{PใREl๗ q Œ๎C   ƒ?ฃ๔1HYUว๊wA@pœ—7L&jบ~บ[ฦ.ปŽฺเ_ว:;฿‘.8ุ a๘๕ฟv; €นA่ึ3ิQ7ภvMธvฑพo๕น็]rไสใ{ฌw*“ุgย/}7ยฯิะ|#8๖ศA์9ะฌ 2ญ โ:d๘พ+ F‡Cœ๚ ๔๓๎cB๋ทวืคu#๑จnAใ ฮห40)๘กื‡ศ?ฃ๓ ‚Pปyฒ Mๆ ฐ];\;_ฯ}๑]๗๋g{"2น๛L่q8€#|ไศรญ/ธ'ธy8<จo™ป’…‹ะ#VนY—_๊$D๐RW๕~v๏ร๕๏G์"๐(พ~8o\:ชO}Pฐ]ง๘„ โ‰r’าเG0ฤิ๕6ฐ(7Œaˆท if`๖o๋็zยํaฏO`’ฝ>ย_wล€๐?\[๋&D6 cโ‹›=™y—tŸา‹ฉโ่’ฺฟ=ตัNอํว่ว๑๚ขˆะ๕๕$ใzฺฯล๏~Y๛ž๖w ึŸMh‹˜ืวๆk8^ธำŸ๙ฦ)0ษ C๘%ฏ๛x\Œfq1ˆ8h\๋&Œ ฒๅ‡;ฌŽ6ะงAน๓๓ษ-:?ฟSยo•็oPF„=บ๘\๏@\๏”@.@K๕›ึ๗๔?‚ฟ ภผ&lืฎo|๐;›๎ดืฯ์Der&ษ„~ไ‘เU’+oRื›wsภ]วทq‚๙›ฺฑว‚@ผU‡ๅ๗`/๛F=ˆภ^pCํฯˆ๋ฑห.v๏Qฺsใิ๕0ขทu๓#x0ล8๏2๔ใกพผBใQืhย๚s๗๚yิณ๋˜ไ๚)|ไฺ่๖yณ[แึŽp+ แk™๘f ”็:ๆC >ศอ€เZRธhมเ„€#๐:nJฎ9\]ๆ ท ขmอ{&@4่8IG0€$โHE4|@TะŽ‚€[w แH๛%๕š˜7๓ลฎ๘f๕ํ.ˆบ9๔้ฆ๘ีๆเอ_zแ‘หฎทหdR“ „_๒’ูฑ๚เ๗zฺz ใ๘ฝcžl 4ƒีo€<šฦฃiึะ4กn6อ๕๚‹ท6ฮ{ ๘Q&0ษ Vพ๚?f๏ อ@}็ะ,nฦธB „เ๋ฆ๑ŸASฝฟ†ฟzฑนฦo๚๕ว|zฏฯ๛tสค&ูยฏ=›ธ9ทqt`ksvญรฺ—ฟ๖ศ%ืป—xL2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$7l๙ใฝ^œŽฦญIENDฎB`‚lemonade-sdk-lemonade-dbde812/docs/assets/footer.js000066400000000000000000000050761516551144000224320ustar00rootroot00000000000000// Shared Footer Component for Lemonade Website // This ensures consistent footer across all pages function createFooter(basePath = '') { return ` `; } // Function to initialize footer on page load function initializeFooter(basePath = '') { const footerContainer = document.querySelector('.footer-placeholder'); if (footerContainer) { footerContainer.innerHTML = createFooter(basePath); } else { console.warn('Footer placeholder not found'); } } // Function to initialize footer with DOMContentLoaded wrapper (for standalone use) function initializeFooterOnLoad(basePath = '') { document.addEventListener('DOMContentLoaded', function() { initializeFooter(basePath); }); } // Function to initialize footer when footer is already in DOM function initializeFooterStarCount() { // No-op kept for backward compatibility with older pages/scripts. } // Export for use in other scripts if (typeof module !== 'undefined' && module.exports) { module.exports = { createFooter, initializeFooter, initializeFooterOnLoad, initializeFooterStarCount }; } lemonade-sdk-lemonade-dbde812/docs/assets/install-selector.js000066400000000000000000000737161516551144000244260ustar00rootroot00000000000000// Lemonade Install Selector - Clean Rewrite // Supports: Windows/Linux, App+Server/ServerOnly, OGA/llama.cpp/FastFlowLM, NPU/GPU/CPU const ALLOWLIST = [ // Windows: OGA (NPU, CPU), llama.cpp (CPU, GPU), FastFlowLM (NPU) { os: 'win', fw: 'oga', dev: 'npu' }, { os: 'win', fw: 'oga', dev: 'cpu' }, { os: 'win', fw: 'llama', dev: 'cpu' }, { os: 'win', fw: 'llama', dev: 'gpu' }, { os: 'win', fw: 'flm', dev: 'npu' }, // Linux: llama.cpp (CPU, GPU), FastFlowLM (NPU) { os: 'linux', fw: 'llama', dev: 'cpu' }, { os: 'linux', fw: 'llama', dev: 'gpu' }, { os: 'linux', fw: 'flm', dev: 'npu' }, // Docker: llama.cpp only (CPU, GPU) { os: 'docker', fw: 'llama', dev: 'cpu' }, { os: 'docker', fw: 'llama', dev: 'gpu' }, // macOS (beta): llama.cpp only (GPU via Metal) { os: 'macos', fw: 'llama', dev: 'gpu' }, ]; const NPU_DRIVER_URL = 'https://account.amd.com/en/forms/downloads/ryzenai-eula-public-xef.html?filename=NPU_RAI1.5_280_WHQL.zip'; const GITHUB_RELEASES_API = 'https://api.github.com/repos/lemonade-sdk/lemonade/releases/latest'; // State: os, type, fw, dev, latestVersion window.lmnState = { os: 'win', distro: 'win', type: 'app', fw: 'oga', dev: 'npu' }; window.lmnLatestVersion = null; // Fetch latest version from GitHub async function fetchLatestVersion() { try { const response = await fetch(GITHUB_RELEASES_API); const data = await response.json(); // tag_name is like "v1.2.3" or "1.2.3" window.lmnLatestVersion = data.tag_name.replace(/^v/, ''); lmnRender(); // Re-render with version } catch (e) { console.warn('Failed to fetch latest version:', e); window.lmnLatestVersion = 'VERSION'; // Fallback placeholder } } function isAllowed(os, fw, dev) { return ALLOWLIST.some(c => c.os === os && c.fw === fw && c.dev === dev); } function findValidCombo(os, fw, dev) { // Try exact match first if (isAllowed(os, fw, dev)) return { os, fw, dev }; // Try keeping fw, find valid dev for (const d of ['npu', 'gpu', 'cpu']) { if (isAllowed(os, fw, d)) return { os, fw, dev: d }; } // Try keeping dev, find valid fw for (const f of ['oga', 'llama', 'flm']) { if (isAllowed(os, f, dev)) return { os, fw: f, dev }; } // Fall back to first valid combo for this OS const fallback = ALLOWLIST.find(c => c.os === os); return fallback || ALLOWLIST[0]; } window.lmnSet = function(field, value) { const newState = { ...lmnState, [field]: value }; // OS change may invalidate fw/dev combo if (field === 'os') { const valid = findValidCombo(value, newState.fw, newState.dev); newState.fw = valid.fw; newState.dev = valid.dev; if (value === 'linux') { newState.distro = 'ubuntu'; } else if (value === 'win') { newState.distro = 'win'; } else if (value === 'macos') { newState.distro = 'macos'} } // fw/dev change - validate else if (field === 'fw' || field === 'dev') { const valid = findValidCombo(newState.os, newState.fw, newState.dev); newState.fw = valid.fw; newState.dev = valid.dev; } window.lmnState = newState; lmnRender(); }; window.lmnRender = function() { const { os, type, fw, dev } = lmnState; // Update active states and disabled states const cells = { os: ['win', 'linux', 'macos', 'docker'], distro: ['win', 'macos', 'ubuntu', 'arch', 'fedora', 'debian'], type: ['app', 'server'], fw: ['oga', 'llama', 'flm'], dev: ['npu', 'gpu', 'cpu'] }; // Reset and set active Object.entries(cells).forEach(([category, options]) => { options.forEach(opt => { const el = document.getElementById(`${category}-${opt}`); if (!el) return; el.className = ''; el.onclick = () => lmnSet(category, opt); if (lmnState[category] === opt) el.classList.add('lmn-active'); }); }); // Update NPU label based on selected framework const npuEl = document.getElementById('dev-npu'); if (npuEl) { npuEl.textContent = (fw === 'flm') ? 'NPU' : 'NPU, Hybrid'; } // Gray out invalid combinations cells.fw.forEach(f => { const el = document.getElementById(`fw-${f}`); if (el && !isAllowed(os, f, dev) && !ALLOWLIST.some(c => c.os === os && c.fw === f)) { el.classList.add('lmn-disabled'); } }); cells.dev.forEach(d => { const el = document.getElementById(`dev-${d}`); if (el && !isAllowed(os, fw, d)) { el.classList.add('lmn-disabled'); } }); // Render download section renderDownload(); // Render quick start commands renderQuickStart(); }; function renderDownload() { const { os, distro, type } = lmnState; const osDistro = document.getElementById('lmn-install-distro'); const downloadArea = document.getElementById('lmn-download-area'); const installType = document.getElementById('lmn-install-type'); const cmdDiv = document.getElementById('lmn-command'); const installCmdDiv = document.getElementById('lmn-install-commands'); const version = window.lmnLatestVersion || 'VERSION'; // Handle macOS (beta) if (os === 'macos') { if (osDistro) osDistro.style.display = 'none'; if (installType) installType.style.display = 'none'; const pkgFile = `Lemonade-${version}-Darwin.pkg`; const link = `https://github.com/lemonade-sdk/lemonade/releases/latest/download/${pkgFile}`; if (downloadArea) { downloadArea.style.display = 'block'; const linkEl = document.getElementById('lmn-link'); if (linkEl) { linkEl.href = link; linkEl.textContent = 'Download Lemonade Installer (.pkg)'; } } if (installCmdDiv) installCmdDiv.style.display = 'none'; let notes = ''; notes += `
Note: macOS support is currently in beta. The installer is signed and notarized for Apple Silicon Macs with Metal GPU acceleration.
`; notes += `
To build from source, see the Developer Guide.
`; if (cmdDiv) { cmdDiv.innerHTML = notes; } return; } if (os === 'win') { if (osDistro) osDistro.style.display = 'none'; if (installType) installType.style.display = 'table-row'; // Windows: Show download button let link, buttonText; if (type === 'app') { link = 'https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade.msi'; buttonText = 'Download Lemonade Installer (.msi)'; } else { link = 'https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade-server-minimal.msi'; buttonText = 'Download Lemonade Minimal Installer (.msi)'; } if (downloadArea) { downloadArea.style.display = 'block'; const linkEl = document.getElementById('lmn-link'); if (linkEl) { linkEl.href = link; linkEl.textContent = buttonText; } } if (installCmdDiv) { installCmdDiv.style.display = 'none'; } } if (os === 'linux') { if (osDistro) osDistro.style.display = 'table-row'; if (installType) installType.style.display = 'table-row'; if (distro === 'ubuntu') { // Ubuntu: Show structured server + frontend installation const appImageFile = `lemonade-app-${version}-x86_64.AppImage`; const appImageUrl = `https://github.com/lemonade-sdk/lemonade/releases/latest/download/${appImageFile}`; if (downloadArea) { downloadArea.style.display = 'none'; } if (installCmdDiv) { installCmdDiv.style.display = 'block'; const ppaCommands = [ 'sudo add-apt-repository ppa:lemonade-team/stable', 'sudo apt install lemonade-server' ]; let frontendSection = ''; if (type === 'app') { frontendSection = `
Step 2: Choose your frontend
Option 1: Web App (default, available at http://localhost:13305)
The web app is automatically available once lemonade-server is running. Just open your browser and navigate to the URL above.
Option 2: Lemonade Desktop package (web app launcher)
Option 3: AppImage (portable desktop app, no installation required)
Option 4: Snap (fully sandboxed desktop app)
`; } installCmdDiv.innerHTML = `
Step 1: Install lemonade-server
Via stable PPA (recommended):
Or via Snap:
${frontendSection} `; setTimeout(() => { // Render PPA commands const pre = document.getElementById('lmn-install-pre-block'); if (pre) { pre.innerHTML = ppaCommands.map((line, idx) => { const safeLine = line.replace(/&/g, '&').replace(//g, '>'); return `
${safeLine}
`; }).join(''); } // Render server snap command const serverSnapPre = document.getElementById('lmn-install-snap-server-block'); if (serverSnapPre) { const serverSnapCmd = 'sudo snap install lemonade-server'; const safeLine = serverSnapCmd.replace(/&/g, '&').replace(//g, '>'); serverSnapPre.innerHTML = `
${safeLine}
`; } // Render AppImage commands if App + Server selected if (type === 'app') { const desktopPre = document.getElementById('lmn-install-desktop-block'); if (desktopPre) { const desktopCmd = 'sudo apt install lemonade-desktop'; const safeLine = desktopCmd.replace(/&/g, '&').replace(//g, '>'); desktopPre.innerHTML = `
${safeLine}
`; } const appImagePre = document.getElementById('lmn-install-appimage-block'); if (appImagePre) { const appImageCommands = [ `wget ${appImageUrl}`, `chmod +x ${appImageFile}`, `./${appImageFile}` ]; appImagePre.innerHTML = appImageCommands.map((cmd, idx) => { const safeLine = cmd.replace(/&/g, '&').replace(//g, '>'); return `
${safeLine}
`; }).join(''); } // Render app snap command const appSnapPre = document.getElementById('lmn-install-snap-app-block'); if (appSnapPre) { const appSnapCmd = 'sudo snap install lemonade'; const safeLine = appSnapCmd.replace(/&/g, '&').replace(//g, '>'); appSnapPre.innerHTML = `
${safeLine}
`; } } }, 0); } } else if (distro === 'arch') { // Arch Linux: Show download button let link, buttonText; if (type === 'app') { link = 'https://aur.archlinux.org/packages/lemonade-desktop'; buttonText = 'Download Lemonade Desktop (AUR)'; } else { link = 'https://aur.archlinux.org/packages/lemonade-server'; buttonText = 'Download Lemonade Server (AUR)'; } if (downloadArea) { downloadArea.style.display = 'block'; const linkEl = document.getElementById('lmn-link'); if (linkEl) { linkEl.href = link; linkEl.target = '_blank'; linkEl.textContent = buttonText; } } if (installCmdDiv) { installCmdDiv.style.display = 'none'; } } else if (distro === 'fedora') { // Fedora: Show structured server + frontend installation const rpmFile = `lemonade-server-${version}.x86_64.rpm`; const appImageFile = `lemonade-app-${version}-x86_64.AppImage`; const downloadUrl = `https://github.com/lemonade-sdk/lemonade/releases/latest/download/${rpmFile}`; const appImageUrl = `https://github.com/lemonade-sdk/lemonade/releases/latest/download/${appImageFile}`; if (downloadArea) { downloadArea.style.display = 'none'; } if (installCmdDiv) { installCmdDiv.style.display = 'block'; const rpmCommands = [ `wget ${downloadUrl}`, `sudo dnf install ./${rpmFile}` ]; let frontendSection = ''; if (type === 'app') { frontendSection = `
Step 2: Choose your frontend
Option 1: Web App (default, available at http://localhost:13305)
The web app is automatically available once lemonade-server is running. Just open your browser and navigate to the URL above.
Option 2: AppImage (portable desktop app, no installation required)
`; } installCmdDiv.innerHTML = `
Step 1: Install lemonade-server
Via RPM package:
${frontendSection} `; setTimeout(() => { // Render rpm commands const pre = document.getElementById('lmn-install-pre-block'); if (pre) { pre.innerHTML = rpmCommands.map((line, idx) => { const safeLine = line.replace(/&/g, '&').replace(//g, '>'); return `
${safeLine}
`; }).join(''); } // Render AppImage commands if App + Server selected if (type === 'app') { const appImagePre = document.getElementById('lmn-install-appimage-block'); if (appImagePre) { const appImageCommands = [ `wget ${appImageUrl}`, `chmod +x ${appImageFile}`, `./${appImageFile}` ]; appImagePre.innerHTML = appImageCommands.map((cmd, idx) => { const safeLine = cmd.replace(/&/g, '&').replace(//g, '>'); return `
${safeLine}
`; }).join(''); } } }, 0); } } else if (distro === 'debian') { if (downloadArea) downloadArea.style.display = 'none'; if (installCmdDiv) installCmdDiv.style.display = 'none'; if (cmdDiv) { cmdDiv.innerHTML = `
For Debian, please follow the build instructions as described in the Developer Guide.
`; } return; } } if (os === 'docker') { if (osDistro) osDistro.style.display = 'none'; if (installType) installType.style.display = 'none'; if (downloadArea) { downloadArea.style.display = 'none'; } if (installCmdDiv) { installCmdDiv.style.display = 'block'; const commands = [ `docker run -d \\`, ` --name lemonade-server \\`, ` -p 13305:13305 \\`, ` -v lemonade-cache:/root/.cache/huggingface \\`, ` -v lemonade-llama:/opt/lemonade/llama \\`, ` ghcr.io/lemonade-sdk/lemonade-server:latest` ]; installCmdDiv.innerHTML = `
`; setTimeout(() => { const pre = document.getElementById('lmn-install-pre-block'); if (pre) { pre.innerHTML = commands.map((line, idx) => { const safeLine = line.replace(/&/g, '&').replace(//g, '>'); // Only show copy button on first line const button = idx === 0 ? `` : ''; return `
${safeLine}${button}
`; }).join(''); } }, 0); } let dockerNote = `
To build from source, see the Docker Guide`; if (cmdDiv) { cmdDiv.innerHTML = dockerNote; } return; } else { // Hide Docker section for other platforms const dockerSection = document.getElementById('lmn-docker-section'); if (dockerSection) dockerSection.style.display = 'none'; } // Build from source note (only depends on os and type) let notes = ''; // Add Ubuntu-specific note if Ubuntu is selected if (os === 'linux' && distro === 'ubuntu') { notes += `
Lemonade is tested on Ubuntu 24.04 LTS but should also work on other versions.
`; } notes += `
To build from source, see the Developer Guide`; if (type === 'app') { notes += ` and App README`; } notes += `.
`; if (cmdDiv) { cmdDiv.innerHTML = notes; } } window.lmnCopyAppImageLine = function(e, idx) { e.stopPropagation(); const pre = document.getElementById('lmn-install-appimage-block'); if (!pre) return; const lines = Array.from(pre.querySelectorAll('.lmn-command-line span')).map(span => span.textContent); if (lines[idx] !== undefined) { navigator.clipboard.writeText(lines[idx]); const btn = e.currentTarget; const old = btn.textContent; btn.textContent = 'โœ”'; setTimeout(() => { btn.textContent = old; }, 900); } }; window.lmnCopyServerSnapLine = function(e) { e.stopPropagation(); const pre = document.getElementById('lmn-install-snap-server-block'); if (!pre) return; const line = pre.querySelector('.lmn-command-line span'); if (line) { navigator.clipboard.writeText(line.textContent); const btn = e.currentTarget; const old = btn.textContent; btn.textContent = 'โœ”'; setTimeout(() => { btn.textContent = old; }, 900); } }; window.lmnCopyAppSnapLine = function(e) { e.stopPropagation(); const pre = document.getElementById('lmn-install-snap-app-block'); if (!pre) return; const line = pre.querySelector('.lmn-command-line span'); if (line) { navigator.clipboard.writeText(line.textContent); const btn = e.currentTarget; const old = btn.textContent; btn.textContent = 'โœ”'; setTimeout(() => { btn.textContent = old; }, 900); } }; window.lmnCopyDesktopLine = function(e) { e.stopPropagation(); const pre = document.getElementById('lmn-install-desktop-block'); if (!pre) return; const line = pre.querySelector('.lmn-command-line span'); if (line) { navigator.clipboard.writeText(line.textContent); const btn = e.currentTarget; const old = btn.textContent; btn.textContent = 'โœ”'; setTimeout(() => { btn.textContent = old; }, 900); } }; window.lmnCopyInstallLine = function(e, idx) { e.stopPropagation(); const pre = document.getElementById('lmn-install-pre-block'); if (!pre) return; const lines = Array.from(pre.querySelectorAll('.lmn-command-line span')).map(span => span.textContent); if (lines[idx] !== undefined) { navigator.clipboard.writeText(lines[idx]); const btn = e.currentTarget; const old = btn.textContent; btn.textContent = 'โœ”'; setTimeout(() => { btn.textContent = old; }, 900); } }; window.lmnCopyAllInstall = function(e) { e.stopPropagation(); const pre = document.getElementById('lmn-install-pre-block'); if (!pre) return; const lines = Array.from(pre.querySelectorAll('.lmn-command-line span')).map(span => span.textContent); if (lines !== undefined) { // Join with newlines instead of commas for proper formatting navigator.clipboard.writeText(lines.join('\n')); const btn = e.currentTarget; const old = btn.textContent; btn.textContent = 'โœ”'; setTimeout(() => { btn.textContent = old; }, 900); } }; function renderQuickStart() { const { os, fw, dev } = lmnState; const exploreDiv = document.getElementById('lmn-explore-command'); const exploreSection = document.getElementById('lmn-explore-section'); if (!exploreDiv || !exploreSection) return; // macOS quick start if (os === 'macos') { exploreSection.style.display = 'block'; } let commands = ['lemonade-server -h']; if (fw === 'oga') { if (dev === 'npu') { commands.push('lemonade-server run Llama-3.2-1B-Instruct-NPU'); commands.push('lemonade-server run Llama-3.2-1B-Instruct-Hybrid'); } else { commands.push('lemonade-server run Llama-3.2-1B-Instruct-CPU'); } } else if (fw === 'llama') { if (dev === 'cpu') { commands.push('lemonade-server run Gemma-3-4b-it-GGUF --llamacpp cpu'); } else { commands.push('lemonade-server run Gemma-3-4b-it-GGUF'); } } else if (fw === 'flm') { commands.push('lemonade-server run Gemma-3-4b-it-FLM'); } exploreSection.style.display = 'block'; exploreDiv.innerHTML = `
`; // Add contextual notes based on inference engine and device let notes = ''; // NPU driver note (Windows only) if (dev === 'npu' && os === 'win') { notes += `
Note: NPU requires an AMD Ryzen AI 300-series PC with Windows 11 and driver installation. Download and install the NPU Driver before proceeding.
`; } // FLM Linux NPU setup note (above Early Access notice, matching Windows driver note placement) if (fw === 'flm' && os === 'linux') { notes += `
Linux NPU Setup: See the FastFlowLM NPU on Linux guide for setup instructions.
`; } // FastFlowLM Early Access note if (fw === 'flm') { notes += `
FastFlowLM (FLM) support in Lemonade is in Early Access. FLM is free for non-commercial use, however note that commercial licensing terms apply. Installing an FLM model will automatically launch the FLM installer, which will require you to accept the FLM license terms to continue. Contact lemonade@amd.com for inquiries.
`; } // llama.cpp backend tip for GPU if (fw === 'llama' && dev === 'gpu') { notes += `
Tip: To select a backend, use --llamacpp rocm or --llamacpp vulkan
`; if (os === 'linux') { notes += `
Note: You may need to run sudo update-pciids for GPU detection on Linux.
`; } } // backend config for docker if (os === 'docker') { commands = []; if (fw === 'llama' && dev === 'gpu') { commands.push(`docker run -d \\ --name lemonade-server \\ -p 13305:13305 \\ -v lemonade-cache:/root/.cache/huggingface \\ -v lemonade-llama:/opt/lemonade/llama \\ -e LEMONADE_LLAMACPP_BACKEND=vulkan \\ ghcr.io/lemonade-sdk/lemonade-server:latest`); notes = `
Tip: To select a specific backend, update the LEMONADE_LLAMACPP_BACKEND environment variable: LEMONADE_LLAMACPP_BACKEND=vulkan
`; } if (fw === 'llama' && dev === 'cpu') { commands.push(`docker run -d \\ --name lemonade-server \\ -p 13305:13305 \\ -v lemonade-cache:/root/.cache/huggingface \\ -v lemonade-llama:/opt/lemonade/llama \\ -e LEMONADE_LLAMACPP_BACKEND=cpu \\ ghcr.io/lemonade-sdk/lemonade-server:latest`); notes = `
Tip: To select a specific backend, update the LEMONADE_LLAMACPP_BACKEND environment variable: LEMONADE_LLAMACPP_BACKEND=cpu
`; } } if (notes) { exploreDiv.innerHTML += notes; } setTimeout(() => { const pre = document.getElementById('lmn-explore-pre-block'); if (pre) { pre.innerHTML = commands.map((line, idx) => { const safeLine = line.replace(/&/g, '&').replace(//g, '>'); return `
${safeLine}
`; }).join(''); } }, 0); } window.lmnCopyLine = function(e, idx) { e.stopPropagation(); const pre = document.getElementById('lmn-explore-pre-block'); if (!pre) return; const lines = Array.from(pre.querySelectorAll('.lmn-command-line span')).map(span => span.textContent); if (lines[idx] !== undefined) { navigator.clipboard.writeText(lines[idx]); const btn = e.currentTarget; const old = btn.textContent; btn.textContent = 'โœ”'; setTimeout(() => { btn.textContent = old; }, 900); } }; // Parse URL hash and set appropriate state function parseHashAndSetState() { const hash = window.location.hash.substring(1).toLowerCase(); // Remove # and make lowercase if (!hash) return; // No hash, use defaults // Handle anchors switch (hash) { case 'linux': lmnSet('os', 'linux'); break; case 'ubuntu': lmnSet('os', 'linux'); lmnSet('distro', 'ubuntu'); break; case 'arch': lmnSet('os', 'linux'); lmnSet('distro', 'arch'); break; case 'debian': lmnSet('os', 'linux'); lmnSet('distro', 'debian'); break; case 'fedora': lmnSet('os', 'linux'); lmnSet('distro', 'fedora'); break; case 'docker': lmnSet('os', 'docker'); break; case 'windows': case 'win': lmnSet('os', 'win'); break; case 'macos': case 'mac': lmnSet('os', 'macos'); break; } } window.lmnInit = function() { const installer = document.getElementById('lmn-installer'); if (installer && !document.getElementById('os-win')) { installer.innerHTML = `
Download & Install
Platform Windows 11 Linux macOS Docker
Installation Type App + Server Server Only
Quick Start
Inference Engine OGA llama.cpp FastFlowLM
Device Support NPU, Hybrid GPU CPU
`; } // Listen for hash changes window.addEventListener('hashchange', parseHashAndSetState); // Parse hash on initial load (after HTML is set up) parseHashAndSetState(); fetchLatestVersion(); lmnRender(); }; lemonade-sdk-lemonade-dbde812/docs/assets/logo.png000066400000000000000000001032411516551144000222350ustar00rootroot00000000000000‰PNG  IHDRปนpXNasRGBฎฮ้gAMAฑ a pHYsรรวoจd†6IDATx^ํฝœๅู‚˜hฌ‰&1‰%๖B๏Eช‚ขุˆฝ—“hKš){คƒ€ E@z๏ฝื…e{ƒฅ.ห.leฟพใน๗?wีไอ›_ะ3Ÿฯ๕ูsfๆฬ™๙>๗s๗๓ฬ์ฤ–ุ[bKl‰-ฑ%ถฤ–ุ[bKl‰-ฑ%ถฤ–ุ[bKl‰-ฑ%ถฤ–ุ[bKl๙š/uอŠ-ฑๅฟfฉ ะบ&ีด๏ุ[ฃK4€ัRฝ(ฅ่๗‘oฝ฿่ฯ-ฑๅl ƒ†0 ถCˆพ)๐%ลบˆํขD4แc‰-ฑๅ฿ถ„มŠ< 7ภžัIาษัฉตศ฿๗๕ู๖[๛๑F๐eภ-ฑๅ_^ขGแแ>M:]๚ŽtfD฿•พั๗kฏณŽฏฯถ์ƒ}y#p๘รเว -๋ฅ6ศ2€< $2[:CุณคI็H็I?–..๑๙๋Ÿ+-ฑ๖EC`฿ภฯg๑™๑๎ฤ -๔๒y–Gpข-๐!0@n€ฝXบLช/5’KMฅfR๓ฤ๋ผXทกtนtฉt‘ฤ>ู๗ฅ0๘ํkƒฟK ๚ุRฝ„กC๎Vลฃ8Yฑaภ‰ส—Hภ ฌ-ค6า•RGฉณtติ5ขnQโ5ฟJb]ถa[๖มพุ็๐‰๚D|์ะำ๐พ,๔(ถ| —0ั ‘๛p‡{แภ›Hญ$ํ"]#๕n‘n•๎๎”๎–๎‘๎ฏ#ึปM๊)$]/ฑ/๖ู^r๐‰๚X์ ๎หB๏ ๏—bห้พHPDC<@Žm ’zว^`O€‰สฅ›ฅฅ๛ค‡คGฅ_HOHฟ–ž”~+.๒ำล๋่Wาใาฯ%ถ}@ขะn”ฎ•ˆD|,  }M๖ฆฆ ฮฟSแsŠbหใ%|1"…!Žš & 'ขล ซ|D`"2`(`๓๏ฅ?K“^^’^–^‘^ ‰ฟy๗!]๚‹๔'้i้7 ๆa‰^ž‚ˆOk'แ๙ฟ ๔แ ฮฟ"ฮQด8wต5 ๐นŽ-ม%|โbp‘ธˆั ฐ8ไุ2Id[ _}tXŸJDdภ๎็%~SzWz_๊+๕—†4("~๋ฑ{า[าk เฏ๐“๎—่AnzŽใ$กๅธฃ=ฝƒO๎-พ?#๒^# j‚?|cหแ๒e ๗า!eC*”r"'^จ€ ศ€h‹๙ฃDไLเX*FIฃฅฅq!ˆืัXiŒ๔‘ฤvl?Xb์๗ ๐iPOI|ƒR4๔ุ๗๔ุ."=฿‹^Š๏๔Zˆ๏ฯˆFใ ี4๐๕yถ)ั>ฑ~ฒร{e<้r์@ ศปUม‡RzFยj`?–0?”˜'JSคฉา iฆ4Kš%^ใ=4MbIา'๛a์—^ ฤ็ay๘h่IŠ้y่€>\ม!ฺSลก1ำˆŸง/;๐ๅ ˆs้ ภแ2เว–ๅ 9'›Oไ‰.9ƒ>$ิท=้ค$ไTQ‘๐โX <8Qซ๕&$Qzฒุ๋-”ุfฎ4Gข? ‡่Oิ@๊'ECORKใžž =RK‰  ุk๗”I ม…ฉI๔ฎ/๘ข?ฝ=€ƒฮbะ—0่œX๎–ศŽไ90P:$้คิGUๅ.ษ๘$ผ๘๋Ro‰(Žี@ข๐t 8pxนด2ขีาš(๑škUDฌปBb[๖มพุ'=ˆž‚F…5ยๆ„กว;๔Tpฐ74Z*84`"~k‰๏J#pัภ?Oต |ัsph,4็•^ภ#?เ{พŽ๖1่ 'Œ‰8ฉอ9้\บp"“Cฮล๏$QYกพMลƒช ฅ@ ว“l’8‘FJxม‰สภ{ด^ฺัฦ(๙๋.ึElทVb?์๐‰D}">ั> =‘EB๛ข๔ไุlฅKข=5ส—4"?ฅาฯ็}ภ ˆส †*ฝ#ว๔œใ0๘ž(GCO0ŠA/,~ยย U8ัxMบ].… ฤE๓ส ตqข"  ษ €a+ˆดD\ $&€:›คอา–(ลE)๋#o a๐‰๚| ІE C๏๖†ถ7ไTpศ3~&aวhŸOนิEo็๙ภA€†ƒญ |‘'ะา:HภO/ภ9& ๑I”ฑ:a่ @ตEz>}-‹Ÿ("'้ sข้b9๙tรD$.,r@yGrฌ~œd‘ไ‘ศJ„๕( ธฝ5คm…+ผn4๘์ำฃพG๛0๔แHO2๋ะs์|r พต๛็$j๗ไ่Y‰ฦ-Jจa๙Xm_4 2ฝฝฝ‘Ÿ^“ˆOฎ€็xu;ƒ_X8:โไq๑Œ€N—Šฟ๔hNwN”โbq!ฺ8%>๊แร%ส€$†๘q‡|™D”uศ‰ภaภฃAŽซN‰?„บ 'Ÿ^/้ด3ค3๋%žzzฝฤพU7กN`0๘๙z๖ัะืfo๘ไ|ฏำˆ๔XX3ส™ไ!aั@\แมฏ่/j ็G}‰€Oฏ้ี!ข}x๐๋‡ะGท65Aต^$pbบ8่œD"I( “”แ9น\"5rไิฦ)๑Q๏&้$!""(พภฐ5E๑0ิCJ8ฎ^ฤณฮ;1ต}๏๎บ๏™s<ำ๛‚ร/พธ๘ญI—–พ;ๅา’7ฦ_ZืAๅ?ฮฯฝ้ก์ฎ฿๒ิ๔ำพs|bบuš <{CA/ฤw|-๐“kะ$z-ฝAXุ6 Š")ฆJใกแะ@h$๐๔T†8ท$๗๔šXฌ็ž๊ษ.วกฏiฤืซ7a?ตฟ<'ยA๗d”Gล…“IJDงk‰ฤฐ;‘ˆ"ั‹9hจ„„!2€ G๑0เ .EpE๏ใ“[\}ฦฮŸใยทง]Q6|Mƒชฑ[ฏฐ๑ฏฐ‰I ljj›žาfeถตูYlNVg›™ึฅjยๆ๏OmY‹ฟ]œำผรw2ฟuสq์; =ภืdoฐU๔<^มกกz‡<Qๅ;บ่น\Xตฐ&DDƒม"ี4๐Eฃ ๗๔๔D~ ็ปCpมๆะฃr๚ฯ›ๆ@ษ๒๓ฌอืjqะ€CT †๎ษ(“JWŠ?'A#๒Paมฒxโษ… '้tศช„!L๔Ž(Q'žqึ ฉ๎p‘}ด๙|~ฟิ&&6ถฉ)mlfFg›ปใ:[ธ๋[ฒ็N[พ>[uเa[s๐Q[—sp๘—ถฉเq้ถฑเgถ6๏A[ukี;cดปๆ๛Y๕Žฏ[“ฝA@OดGns~ข~X4†h๑]๔.,็a•7€๔D~ภ'โ{ขŒUฤใ?-…กง ๊žYช7”,ฯ{”Zฺš0่ต๙tF๙๔`ฎีชLฑฅ{ๅไˆHDs.0r (ท*5B^๗ธ:I?ผเ[้7?๖ฃ}ษ‘ห/ฉถ๎|ฑ๑\ต๙ผ ขฏ/ะ[๔๖63ฝซอษ์a v๖ดEปฝ๗+ุฐ•9๊ŸฺฺƒูzฟQภo.xยถฺโŠญŸ†Cุ์ไ[*žxพAฮwพ๗อ$}>วๆะ;๘ั?๖ิ&HXœัpย= มm=ƒƒ]โาkrฎฑŠ๔คx๛h่ฑ7\#‚ีฆNปต๑(ถ5_+เร ๓ล้ๆ่๎4"ะ2ฐABJWษษฤ7R9 ๚@ฤม{rQธH\4.(0JM;เไJ:“ฮปโไฬ;ž<๛เKใ..ธ์b๛`อy6l9ะ/ฐฑq— ๔ฦ61ก•MI๎hำSปฺ์๔๋mnๆM6วOla๖ํถxื]ฒ+๗ุ2 ฿wฟญุฟญสy@เ?(๐๘Gcถฅ๐็Š๖ฺส}๗Tฝ5ชํแ๓/;9Uวย1:๔.‡?j’๗8ขๅฅถs0ย๙‚W‡ˆ๖ุ‡ž`๔Œ๘ˆ/‰,๙ๅ`สยXข[ฒ๛vมปเฟ]๐฿.๘ow;eg๎ถ๕‡๎S„ฟ_–็.4ซcัๅMOOืqqผ.ภห@๔๋แ๗jSm&sธ'๐จ]โ|GCžๆ@žฒ%S4ฐ6ฬ๋!สSตแzbkฐง๎ใฟภ;่แจ่ัีข:C๘B"I >“Nยลกkฎ ๔ฃ ?๑คใ’ถ?-๛งฯŸs๘ต)T๔[zž \qถ@?ว†*ขวบฌฟุ>ฺx™ะ>Žkjใทตถ‰๑Wฺไ„ฮ65้j›ž|อLน๎Sเำoดน7ผฬ›-๒ไทุยาฮ›Mถ(๛&มฃเฟA๐฿ ๘{dwnดUนท๖[ๅ๓{ฺเ9ํ‹/itJZไ˜ฃnŠjka๘k฿ซC5A๏‘ํ ม‡Šy๕zซNDyฎ6ิm €งฏ ๘ฏฬโญ—/ฦ—ไหีiํัQฎมฐ7ๅ/Bจ ี9แtตtฝtว\4.จCR ๙ษงืKiูํ๔ฟxํ์ยืงŸWูgษ9ึููcบ๚พ๖b๛pe6rC}ตฑ‘ูิ>าาฦomkŸl๋`“ทwถ) WดฤnฺOOฝfะ฿`sฅyจ‡เฟๆg^งะ์ธF  ›@WมตเฟJ ํU๒๙Wูฒ]mEฮ5๚y๕šิผ๐G?>Kn 5ช^ฝ:‰฿9๘ไ‹~|RZ็6gdsใw๚ม๓๖ๅ‰ ๖ฟ๔๔E9ฏ={ัWžน(็๏ฟนp๏9o๏=”๕ส33/9คm—X๏ธ:œŸšเ!ท:nsz,ŽC๏žžฺ?5|ช7”,ฑ6\/j๔*ๆ่Pฑ!y y–ๆ+ฑิีร5u๗๊แR#]#ƒั@E๐๊xI* \.‹ A๙๐๔๏ึKkห้{‹_Ÿ~NU๏…?ฒพKฮถKฯตหฯทม+/ดV]lรึ\*ุ/ทุ๋Gู๋่Mmฬฆๆ๖๑ๆึ6~K;๛dk›ดญณ€ฟJภwตi >้Z›‘fJณRบ์T)ญปอIปV๊ฆ $6*E.‚ฟ“อห๊(฿^Im;๙ถJlุโmพดทญ-ึž๋{qIงGาzวW'๑{g|3นcซ3ฒ~๓ะ๗ zน~ขQ-Š“ๆต+฿ฝบ}ลมชŠิ ศj•%ถท๒ไˆR๔ZR+ุึพ๊ภฦ๖Yหฺ—-ืชh่๋ ๒ž~๔‚=Wต=3ใฌ๏ž$๘ฝGpดw่ร‘ž(ะSzฎ ƒyXz_ง˜ถภYุ’W,้—โ1ฟ„a๗ ฅFฒuข:#ฅx<&"1[ศภฬฟฺ, ‹ย…โข%P]iึ๕ไฟ๎}Vษซำ~X๕ฮผYฏg[ŸE็Zฟ%็€eฺ ๅ—ฺะU—ูฐีWุ‡kุˆตlไบ&6jฝ`฿ะยฦnjmใ6ทต [ฺฤญ|›/เท|W›N”O๊f3ค™ษRสี*์ขจ฿Y๊จะ^ m;y6๒๘ญlnV ›ทฃ™อ฿ูฤd7ฒ…ปูข mัž†6+ฝ~UฯGฮุซ๏XทNฤณฮt๋9;/<๗คไใ๋ีXญ zฏโะปr-(ธต!สS&ฟbื hัภป‡็๚|ธฬ๏_€/ma๘า>›ัSN'‰๙”บ({1ฺวศ •N8’ โ&แŒ~#ํู!–ฝ>ใ{k๖y๖๎๓ฌ๗‚ ๖ ฌ฿โ‹lภาKlะฒKmศ๒หํƒWุฐU๕m๘๊†6bMcนถ‰}ดฎ™^/ุ7ดถ7ตต๑› ๘Ž61ฎณMฺ*เท ๘๘ซm๊vูฌ4=๑*ฉณภ๏คะAQJ›‘าVjm3S[ศใ7ฬmvFC›“y… ฟฬๆํผDภ_$เฅฺฉŒsŠปด?cว_}๑ลcZ—dฏ์TYืลJeฃส“;[ej'ณ๔Žปรง๐ฒz SOa๊)L=E ฝ.ฝทW๋์“๖ ๘ฝฺfถฃ}์๊ฌmปXUึีVœุต*iaง#C^k|๐ๆnge}๛ดใ้U8ง5ANdนX ๔ถ ์‘ภ”ฐ5Tl…ฅD้ภcOนฦ\kฦRผ,้sjยภณ‹ƒถ0$)แฤ”;o(Y1Ž* ~้ญิึ‰L `ฤ.ิa็b‘€=๑ไoืKอป๕Kœูฤžuฉฝ;็B๋5๏B๋ณเ"๋ท๐๋ฟ๘Rธไ2ผ์r_฿†ฎh`รV6ฒW5ถซ๛šf6j]Kณพต€okใ6^iใ7uฐ ›;ูฤ-}—๔]lŠ45^ษ๋๖N65กƒิ^พพญิฺฆ%ตฟojำSูŒิ๚63ํ2ฑอสธภfgุๆdcswH๚<lŽ๓,)ฉqUฺŠ+eMบXQ|+K๊"ภeบคˆl;้NมบKะ๎ผ{€YP๏“๖K9@z๏€ GนZ7Wไjฺวํ+G๛ฬ้ขmฏา>ฎึ>ปZ•rŒm*i[๘ห๛ฮ฿๕ƒ๏˜จs†เ‘'ฒa?๏Qž|Ё>ำ๐๑<๗˜X†‡g …ม'&๙มv&พ= ป[Z4ำD๙ยX๗๋>bส|Œ0์œ@`Gv‡h„็Mn{๛^™ะบ๊ํ™MํYWุ{s.ตs/ตพ๓/๐—ู€E— %Wุฅ๕ํƒe mุ๒F6|ec฿ิFฎn.เ[ฺ่ตญmฬบถ๖๑๚+m†6~cG›ฐฉ“}"+๔“‚H฿ษ&oํ(๐หโดSฤo-›ำRพ™ภodSฏฐฉI—ุดไ y6=๕l›‘๖›‘}้LYœ3l•~ฯศธศeดฒ’ดNVš๔iฏrศณๆN)›hLT&Bญณภ-ๅJ‘หำ:yZ7_็kป|mH๛9$ภ ๐|ž฿U๋Hปi๛kด?iฏ”J’ปWญ™ิก่๗žฟ๋;ง“sถ7akใQ~™D”gD–2%ษ+ม‰ขภs U5fSR‹ง,ษผfณ2๐๐ั˜9&รฮrุฏ3๘ภ n!ฃF ;๎ู9กœ\`็8์ษRส๑'ิMฟํ7zkฺ•๖๎ฬ๖ฌFึ{v}๋3๗rน๕_p… \T฿/n`C–4ดกห ๘&6|E3ฑฒ…\ีสFญncฃืดต1kฏดืuฐq๋;ฺ๘ lยF)€^ยเ้ใฺ V๒๕อฅฦฟพฺKmrย69๑›œ๔C›’๔=›’|ฆMNถ~?อ&}ว“ฯถƒiWXifซPไฎฬไž!(|G๒@.hฑ"X’‚ู!ฯm-X‘~ฯ“๒๕:ค๕i›รฺถ@๛)ะ~ yก /เ…‚บเZฉปtึ•๒ฅƒาiŸด๋z+Jผฎjˆv7\๕ƒฬo~:อ!lmผ\้^'1…เ=ยS'iฅเภ !3(™G€ฃ0NXi๎ฬม‡บ'งx7บ4ŸฏN%ุฑ1L5ฅœE‚สศ)'า]g8ฒำๅ;%ผิำฟ๛ฌGŸoX๔ฮดŽ๖Œ6ึKQพ๗์FึgNC๋7ฏ Xะะ.ldƒ5ถ–4ฑกK›ฺฐeอํรๅ-mฤŠึ๖ัส66jU;ฝบฝ]ำม>^Qะw๔ฝภ"ฝผ|'›[ ฆ๖I\%ณ—ฺ'.ฐOโฯ‘ฮฒOถื&lŽtบpjีGหNญXฑแ{U{’.ฐขดVฆฤต*KP*yฌv%[ึษ7;่yR> K‡ดaญXh๛B^(ะ‹z‘ /ไลผX0๗nnิ{RกT ’๒๔๚ฝฟWฺyƒํ฿ุฝโฝฟ4ส9๗G฿ข๗$สGฯต๐ไ5 <–†z|/‰๒1#ฎฬZฅาFŽm๕„•๒sุฟร‹ฬ,ตม๎๕u‡J ƒIภฮํb A3Hมsฎ™ว$%ฒN(]จรX ุคI๛น฿ฺ๙๋ืš–ผ;ญณ๕šqฅ๕žูสŸีฬ๚ฮibๆ6ฑ๓›ุ Ml๐ยf๖มขๆ6lIKพด•}ธฌXฮ>ZัFญ์`ฃWuด1ซ; ๚N‚^ZืQะท—ฝQปฑฅ<}y๚+l– ฅsl|Y6.๎L๛8๎๖๑–ำl๘ชSช^่ค’7๚žZธzห&\lลŠ่ๅ™อไ“'ษfpF”-Pw ุภ“K'ะ9าV‚\:Qž”ฏืบ่!ะ‹d[Šอ‹eUJษKyฉ.ะฅ7™นY?o‘z๊=ฉX*า฿า!ฝP๋ๅ7Xyฺ ถ|\วยฎํพ—^งฮglMmภciธfฬNฅwฆ—ฆท&1ŽBยส Sด?fํฬ—…์ดxT2x|%,2๛๐ ต^ผ"':lc‚ศ.ภŸ}มษ;๕j๓’๗ฆ\mฝงwฒ๗gดณ>ณZYฟู-ฌœๆ6p^ 4ฟฅ YะJภทถa‹ฺ๐%Wฺ‡K;ุศeํฃๅlิŠฮ6zeณชณ  ๏`cืถ๔-ไู้ว.ฑ7ž'ec7ic6~Fo<อ>Xzjๅ๏{ซ่ฆ{Nศy๑/gไ',;ฏโp๒%๒ๆ—[EF#E๑–‚\0ๅA)จฌH^Yู+ˆ๗ ๆRŽt@๋็FtPส“๒CฐึถภฎSจกH= z้๕’ภ=ไ‚น์'าญาmŸ๊ศํz_*ั๏ลzฝP๏ึzyjmปใFหZvMูใ๗\˜}ย7ฒ55ฯศ+–“Z๐๐kฃฏฉzjw๋;Z๋?ฃซ ˜yต œu• šs• ž{ต}0ฏซ ฿อ†-ธฦ>\xXt\า-ฝ"Šถฒ6MํฃU—ูGซ,e#WŸiฎŽ๕™yzลใ8้pฃ6ว๏>„:้อœœ=m๘น%9[/ฐ‚๔K๚ๅJ@*ช{s#hณฅ]าniา๋{ฅ}า~)G: ๅFtPІAฏถ/aะe[ะ#ัศ+s… ฎธวฌ๒^ผ๏Wน.ำ๋G‰ึ+า๚j๙j$ีภ๗ฐโ๘ช๚ฃi๎™฿ช5Ÿ{๖๎ฤ3ห๙รi‡.o๖]วฃณำ:ท9m๗ข๑ษ‰ปะ /ฒา”Kฌ"ํ2~…ข:ฆ‰`o&hคli—ด[ฺ#ํ•๖I๛ฅœˆHนาA์Rพ`?์๎ีฑ/๎ำIF๑่€‰ๆี ์ส๛ฅค?Uา฿ๅzฝL๏—jฝญ_ค(X็i?9>ฐ4=์HBช^i>บJƒีค˜ภุˆ'ฌTี๐๏ R€เ^๊๏โๅHฆzืV๙ฏ_๖่า#uv๊ฌ|Anมฃu๓ฅIR๑ํT/&…ฃ;CำxwF๏˜ฦษๅdำญ†“ีฃกฏ#[s๖ฉ;nนฟI_{u/{olฯชพ“zZฉ=mเ๔[lะŒ[l๐ฬ›‚zA฿M‘พฃฌขฦาฅ6tั…ฒ?TใƒsŽ๖ุ™|้ ;๋ี๛rิฅอ้ป—Nธค,G kAย…Vš|ฑ@ฟิชา#ฐg)ฒ๏h,ะ|ถด )า๏–๖H{ฅ}zmฟ”ัฝ–‹"ภูุฑ0Š๊A๕E๖ฅP U@วฃใฯ‘๑ไeQดฎPไ ุUIG๔ˆ^ำOTกืหญ๐ลพP ๅz†ƒ~ฟะn5คฬ๋ญt{ช/4=pฺ)วป‡Oฯห๕๑ผ๛wฎ!๓p๚";l๒ภ๙_ฝ„aงฅ:์ั#จŒฌ‘คโแGd๎Œภ้Sฟ;CแS%๏จ(_ทn๔๏เด]ฎฟ"็ฑ฿w)|กOฯฒทG^ูg]ึา6`สm6hzOมƒเ๏nงuฎ๊5ก]ล?4?๒ศ3—j{๕๗๗~๛ป฿ฬฌSงN๕>QfงํZ4ๆฒฒ›.ถร/ดู—ŠิKฌJฐ[†,Lฆzปข๛N)[ะ๏’v#AฝGฺ+ํำ฿๛ฅjุฅvษmLœ*ชSS๊้๒้ิั‹zฑ@/TYސ€สŽ” ฺภชษ\pWTz๔hU๊ต ฝWฎuสิ J|‰ถ-า>k_‘๗฿ง่žญฯษธŠใฎฏzใ™†๛O๘ๆq>โ๊ภ‡ํ ื Nฏฬฤ1 ๔ึไdุสอXXฏฮ๘`ำ1ฃa๗้H;Ÿใพ’w)qศฺ‰๎”ฌ(C’เ0Pมโ‰œTNฐGxท4aเ‚z้฿iY šxw็๎rnบณีม;nŸ๗ฃํ๓oธM w6อํะ๕’}—ิ?+๛ด๏|+๓ธใ๊VGqฉzŸ—]pาฮi\^บwevh›@—})—}ฉ’Wท ช;์ ป€`—vIภพG?๗FดO๖x ุเ#6&จฦ(ชHฒ/‡4bภ่A)ฉSJ,œGi™ข9ึ„ˆ]I๒ŸIE๔๓ˆ๔{ฅ^ฏ๔ๅZฏL๋Qใ(Q#)RฏP แ์ŒG๗=๚ฌื™ฅvทu+๗เลป๋ึญฎรป๗QVzbใ%ัv†๊]ลา๚`ำ1—ฌrpผ๛v`๗$5\เVr`ฎ๐_Hฟx8ฐเOHฟ๚๔g%าkzฟBภ—ซQ”ฉqฐkลฺWปผ{žผ{ฎฌฬ~มพ[ŸปSฐง_cU‰ืฺฦ‰K]z:็'lg|ื฿รv†๊ ฅenํ#จQv&oฃ๖Žฝ๕ฉภว\t็ kณ2” ™#CๆัŸฮหSภHV9๋๘wNำx,?m€“สษๅDaชงK}mเฃš ghฅt๕™{ทLoRนํv8๎R+Mธฤ*’yŠ ข:ๅFช0X˜0์าฎˆvK{P๖ฃ€ภTeีƒฉ๒้ๅำ๓d_˜ะuจป x…ฺxฑฌK‰เ,คGฐ#ุ"ถ ฏj^๙๋(๑šซ๐ๅพL ใˆถ+UC)‘w/’•)”•9,+“/+“KeFkฏฌLถ`ฯRƒKํj•๑m;ญ๒O?5จมปq๎ีท3ไ]L!p๑42zo’U >•€นS_‰่๎%Hฏส„ฃ;_–{๙โุบ8|ฺ;]Ÿฯ`/ศ5]%]&SP9ษ=‘ํMtดฏ8๊gmJ=๏G'fMุ tืสF–ท้r+Žฟิส/ตชdAžZ ่ตม๎ภWร.ะƒDU ๏WTงŒค*ช3m๗ ์Kž์Kพ@;ิC*าR'‘$ `หˆิDm`ุฟุO†คฟƒื|นึ)Sฃ8ขmJตm‰K1ฐหสศส8์๘v`฿'ุw๋vจัฅ+oH๊j๋ฎญ๚O/ูSทNตฏษฮPa<ƒMXR’Uj๏รJoN šŽ่N)๒˜๎<ƒŽ%สฏ๙า”ฅ่๐๏ิeฃง$ษ #้ม ๚ยˆ DN0‘žฮ‰ง‹Ž๖แˆy `W"–๚‡วd_6ตk๋[Aๅvd๛eV™$ศSyš O—2yub*ํศี6F?‚]ฏUG๗์ARQ}ฟขz0?FQy้นฒ/Lฯอ“` #$…TNz‰@V -่e€ ิ‚ปโทQโ5ฝW.เห๛ญภ.+ใฐFร๑ํ๛ิะ๖จgษ์™j|ฉWYี๖ฎ–:๋ชฒ+›I๏่ภGFWนFžฌ๒ดฆ`Uฑญ\sŠDwzc2บ{ซ G๗ฐwงฦJ้ษํ ฅHJRTgjKร#๊ถ :1[ร‰ี5ˆ&œdช6œ๔ฯ‹๖ั: t)ตYSณWmTพgeCห฿x…oฝส.WT่ฉ‚ECOคถ7_Tนqุซํห๑๕๊ฆฟ๕‡ ๏TTฯ]ำภ 7ีท#๊[e‚ Oฐฉ‚5]ฐfึL‰I_มฤ/i'าk>U€ิ@ฺฆ|]=…@Q=˜ ฆจพGQ}o/›ฐ_`ๅศซc#rๅี ขnพ < œ@ชจ^$KRฌh]"Kuฉเ.}F`ป๘ ๖Rึ์4ŽbมNc)RฃqุG`ฯำgŒภN‚บ_ฝKปl ฐg vn Oึฑฦwดา๕]ํต฿ึฯ9ฎnฮkMั ใัYญแ่ฎฬะำ‡ฃ;ฐ3ั= |ุฮะr๙Bdเ$&ัภ3 ˜ึฮ ฦึ0าJ”gp‚9ฌ ะS“Cฯ %Š0ํ”“LW๊Uท4aŽ๊iอๅีืNhRฑจพพ•li`๑‚›4‚›4"bJ๏Nฝ(ฝG{๘ACะ๛Lอ์ิ่ฐG•ธ_ี#{ถzvE๕T5ฮฤ+ญjk{E๗.6๐๏MFžMใัs][tง2รฤ?ฏปุtว์œ™0์Ÿผ[Z5CศL) Cw[รH+Cฬ$ฏxyฌ 'ˆŠ ๓แ๑๓ฬซแwฌ S ˜[รIลา0๕;Ct๗้ยtน{uT?็ฌณ k|d฿Šฦvx}C+าะ*ใhฒเLœ้‚3Cถ#S๒า฿;๕^ ภ‚›S-^ใ=ึำvล—*€jท๚ขjO'๘r๛มž+0สยไB€ ฺC‚๗ฐ`/ฬ…‚บPp ๒j๑ท^/า:๔๔4mX๛ก—8ค"_๛ฮ์ีƒไาธ๔น๔*ภพWฐ๏QOใฐsฃ8ฐง ๖$Yฏ๘vVฑฉƒeฮ้\นลw™W๔yั{ษhDw*3ฬveT•๋๊sf˜GUŒศ๊ๅŸžค•* ฃฌฬ๔(ŸใD=่)Q2žปฯcmยQฬหเฤr‚IX—I$ซ$Qัฐ#ชw๕๘๔๙อซฎidEZ๙ึฦVต]p& ฬ™&03v๐„.]่a้ตเI]่ƒhฏฦแ๖&P๔@ผวzlฃํwสU]lท`฿#่๖สC๏“_ฯQ๔= ุs้Aมš'h๓๏!A|Xฐิ‚เซE !ะ ิ0 daหยะ3า~๒ๅื๓ไื๓่5๔ุ%lSL๙ฝ ฐ๏์ปิใ8์<ศ ุ“๕ททตชธvVฒฎณ yกI^ไ)5E๗่บปช๚œzqฎ7ืŸuxพปร~L๏ kxZ0Uส’|I๗๑ti >แๅ} kCKRร4ฒyฌ ภsาจุ„gN๚D2ค'> {ต…9แuำผpIมๅMํ๐บFVบนฑUnSDOฌษ‚2MPฃำEๆbgI;\ฒ <†h#ฝฏ$ศ๑;ฏkฟ๔Yฺ6xH’`ฺ)ุ™ZปKฐํt{๛^ธOฐ๏์9ฒน‚๕  อ์๙‚๘`>>€ฅื ๔a ƒ@&_;”GO!ุsๅืs้=๔Y9‚๛ดOŸฯำPข์วภฎ๏šข๏š ใ฿ฺฦส7ดท๔™ห;6;ณฆ่๎ำผ๎N!ม็ฬ`Eฑฆไgไk:`\>Ž™2ค/aเฃ#ผWihษ|A๒tgŒถบตzก8!$4ฬซ!ส<ถ†dว๏zยฟ3…•ม'21ษ– ์D’ำj sัน฿ฺฑๆใ&ๅนซ›(ช7ถ๒8พ]`& ุ]P"7O๏ ž๛"หxศQ A€ธ@ฤ>Z`g"Aˆ฿ต_ฌฝDฆถaŸ ฺ์P)ุw ถlมพK6fท`฿ซ่ปOp๎ค9‚๕€ อUค>(ุ๓sพ ฮ@_-ร๎าบ๙j ๙๔ ๔ฒ0ear้1daศฏ็่ณ๖หฏ๏์{้YHN#5v’ำ๚พgฆ7 Ÿ๓’(ลทถชอmญhM'{ฯึ;ฺป‡+3ุIŸ3รเ –“๋ลx ท๏E2s‰ช/ต๏u๘ฐญ!ส“ผบตz*6x:nมฺ8๐D’Wขั,๏ฮM t—t›_๛ืต/kA‹ชCk›(ช7QT่ 29z๐F.6ั (น๘."2๐ำ<‘›Cิ€๐ฺ‰m ~็5ฝวƒK3Ob8>Sฐg ฒ‚}ง`ฯ€ปuw ๖ฝ‚sŸ /Xsํม๋ภิy‚~๐“F@cะ:ุž<5ƒ๊r้darต?r5ฆฒ0๛ๅื๗ ๖ฝ๒๋š์ฬ‹vพ'›g฿pผภžช๏—ค๏ฐ]ฺาฺสึ]i‰S:”5ปโœำ่่๎sf่eฉ”ั๋๒$8ฆt3pH‰™›1‰*=1•จ†—hเึผGyŸˆ๒@Oข‚ต๑ึญMx">Ot'B`e8”"ฃaวณsจณWรฮIฏ=uQ~ฮŠๆVดพ‰•o‘ทŽ”Iบ˜)ะ3tฆบ๒,%j;ธ๘.‚@C BG€'jใ๑จwบ ฏ๋= Im—ก}d๖LA–ฅศบCฐ๏€ูŠบปd5vหr์Qt฿+X๗ ฺ‚7GEษิ่๑;า{ตN. C $—^A Ÿฃํื~๗ซ1ํ#7…ู‹uŠภฮ๔]‚y1Ÿ ปพหึVVฑกฌ์d/>qyNไู3}ฮฬ2‰๙LLๆฃZฦŒV/C†Uzrฆ•๔ฦ์cv–ฺ€Ž๒5YN ภแฑ4xx;ตxช3ภNdwุฑ1๎ู‰0ิูฝฺฏ๛”zำ๚7(ษ[ฬJ6*ชo•ีHะLๆ‚๊โ2$S5ฆฝ๊A๖๊ณ๖่3wซWู%ฟพK฿/[฿mง9 9ฝ๖d}ฮีึ–Vตฉ••ฌigkFต+=์“8ฟ๏ฃช>#า็ป๛Š žจRt Qๅฺ’ท…gCsV&ผิฝG๙hkC”งkรห๛D2บ<ข;Œž๚ปร๎ฯกa<‰ž‘ร8*9ฝไว'ํˆ›ฌ๐ฺfVถน™UmSไMˆ)@จ‹œ!น[‡[ิvเงB ~ืk@ืx|7v‡€้@-0RฅฟI๐่9R๕9<ฒ:Uฆ ๖4ํ/]‘5C6Sf)๊๎P๔ฉ(œญhผKฐ๎ด{๏^€W„w่๗ฝ4ฤ๋Zg?=ศ~5ผ>ํgึHfำrY˜๚ฬ]jhภžMcฆ๗ชvivฮูๆ–ฒ2m,gqงช_~๎ศนGw/C†U RMใžฎ%ฝถ฿ษถ2ฮส1นิผCŽ๒|i€๗A(ฒvVผ;๓iˆ ภNํ–“~ 6'—:;ษภGNุปท?czeั๚fVฑEQš„‚–9= ะเNp”๔ `‡ภ ๒ำเฝ™0…I๐€ เ!QบKึ็0๔žข๕Sด]ชเJีพา[บภหญศˆ™ŠพYŠย;ณ•ณ.ฟ[ภ๏ฬ{<๚j๑7A ๏ฃ'ะ๚{ตฏ๖ณGg๙€ำnูฅ]$ฤ๚ฬl}/*B๔ZXถุ7z7มN’ž ป@ว๖mia•๋[Yแส+mJฏ–…งrKfj ม๕ม‡cQmL%๑“ื๔^’ึIึบษฺ&E%Eฅjฉ๚ผ4}Vบข{†ขoฆขp–กจผSัเณ๐.ฟ[@#ภเ็'ำ๚zญฟคํIxฉ๒์" ึgPษๆ;าจiภุณ๔}2ฃaื๑ Vฐ'F`kaUZZษ๊6–1ซCeทv฿หิ๙๕่NTษŸฐ–๔บ๔พne/!haMฃซ2ppL[™๐ }ุึแ=qๅหำโ้ๆ8!aุ๑์Dฆำ=2˜9D‡JฬQ#ง/3Y &E๛Jั~S๕i๚ผt™.033j–€!เw เ9[@๏B€/?๕w zญท[๋“ไโw‘(ชgซe“๓๘>๚ž;ิซ`ูฒ่ั"=ฐS1JืฑR๒ž*Iฆv๕Šูš––ฟฌฝฝ๕tบuƒ๋ไV†ž5zDีญ ึำซ2L Oเบร€[™cv_j;V†ANDvTช1ฐอP4“ยช’yู‘ได๖ใŽซ“๗ฏ—.Xำส*ไ9m›. ๐a)ˆ๊Dl€ at้o*&;<ี“LABใ`; =C`OิK`UฐFเ.@—อIิ๛‰)Q@%ช‘$i๛$ํ'Y (E๛Nัgค ศ4ูt ๐>Sเf เy‡ผ๘N๘!ําkxlz์6๏ฏจพSgง๖Iล‡ฦห๗ฎพถอmYP%ชม–ีm ฐo’\ย WดฑU#ฺ–ž๗ƒฃƒ ์$ชXJFT™สมS€ฉสpอ่• คเภ bxž{t ๒+ณ8์ศa็หึ;ฃช$5ŒคrwฐSฮbณ์๐†;'+1ภžvโ วฅzํŠยขตญญrณ.\ภ„e&$ื•"ฤWฮทG/แ่Eฐ“ 2ฐD๒'Œ้D jนฬวเฤโรฐๆ”“๊eL๊ี จd]kซฺข‹ฏ ™ค Kข˜Ž-l€ฐรA'บEฏg Dส„Z—* ฅโปตzผ86%ˆเŠ๐๙vคฟท๋๕ํzปึ.ุด]‚`Kะ~_ข Lา1$ ส}nช"rช€O๐้‚7]}F๚|คฟัh,์ถษย๛“๐j_Y๚~™4X? –คํ๙GPีฑV็X`WT฿†ฝฉU ๖าU-,oq;๐—†y‘ูžจzU฿ฮMูL็เfzชgL๕— ฟาพ=ผ|์xvT‡ฏว‰b2ู=Y>]$ฐsb~Tู๑ิ“๋eฬ่จธt. ๑ฮฉŠj้D8ข` €+?QQ๓:ต๐Lญ“!`า'•”Tู€ํž$เi@X• ŠxD๑๚;ื็ฦ ฎxAถ]ฐmื>_‚Qข๖จฯHRใJึ็ฆ่Rmชขtš Nฬ@Ÿ๔as2๕Z ญ“)ะ3๑Š๊Tw2ตŸLํ/#r์4ึ +VLŸOอ?Mวใน%RJฅA‚‹X`gฤ9ปญojeซšูกฅญlอศถG"5wฮ=62<}€ย9น•๛vnะ!p1šJตq•šž>๐ตฎj ฤจณใ๑€9๎์ง ๖YYฏ‹์บ ภI๙p}`ฐศŠkG$:ฅAญC™raš€กš’ข}$ ˜$ฌ‰€Iะุ"y5ไHฏmำ{ดฮ6ญปM $^ %^ะลซแlืqlื~ดD}Nข>/I-9|ŠNศฉ@_ฟฃ ,ย๋k mGu'C๛กา“9nพ/ "lN ฝžŽด nบถKื1งk?Tz(oฆ…)฿=ฐ`:r’lrŽdz=มฮไ/`๗จ์[;ำ,65 `ฏ`Bฒๆ–ณ ฝ๙๚นu๋ทํน•แZP‹๖ํ เ#Q2#%'U …๒aฒภกบ’จ}’|โหฑ,A$@Hฏว ๖8ญ'ะถj›ญฺvซ๖ฑMnำฑlำ~ใตํ๚œํ5AŸ™ ฯNิฑ%้8’t<@Ÿ ๔as๐๗ด nช@งบ“ชคjฉ~ผ‘J?Eวฌc๑|ƒฑชIŒ,3ญ—QำํTaBQ}‹@฿(ญ๛๖โๅMํเยV6ง_หโ3?”neยฃฉ๎ฝฮM8”™อJฏNRaพvฐS–rุ๙O๎ู=A Wcˆ Tขanฐ๐๕—ฏำ…v".`*ะ0จซaWดt่=ูซถ‘Hษ€ฅร$ํ‡สJ‚เŽ=@๑DpAเไz๖--N๋วiป8m'ท ฦญฏ}o ๑เทผŽฯกO๚0๘RŠ<พึ!มMั6)ฺž OŠ๖—9ึไHใคฮOƒฏฮ5จ"%่0เฦ3“ฟ(7n‹Du`฿์ฐ7ถสีญdyห[ุยโวทญhเ;>ฯ=์—IŒns๛$ฃžคด|pษŸ<€}%I…˜€ 9ๆ—hุษย฿์L๛vv’/=Rณ๕HๆuvŸ๑x์Rฺปฯ\š_พNs‹€`ง ฏ v=$†๐ู$แหะzxเดHดL ”-H8ต฿xAX„๒-ะ๕>ฐop[mปE๛ˆŒq‚2N๛ชoจ๔Y๑>^๐nŸ ใ@๘6'Kา:ษZ7Y$k{*)์+[‘qะฟ๖ฐv†›๑|ั#จ$Bt›ัฅว๖?=|~n๙Z]ะอบ x่G”vเอ(D๐v%|aQสฃย‘กฦ€NLiDM” H‚4Q๛JHTXจด€nLe๐ˆ6๋๕@Zgณ`฿,ุ7kปอฺ~‹๖ณEPnxEใธ๐[๎6}&Šฬu (?ขDฝH๋$j}ช:‰ฺž’fข๖—่ว้๘ j|ไAYT`;ฝž`WT฿&ุะ#ๅFผzี๚zim#ซ\ีศJ—5ถC‹šฺžูm์'/?Xƒoง๎I*ลzcnผ๙ต„-e$•ษ~”™™&๒•ฌศ3ฐใ๋ศ‰Œ 2]€น1ฉDืศ4ำ€.๔3ำผ๑G๛ –ตฏฒMบจ[Cฐรฐ๓R€OๅตkJz^ๅ ๑รc bP˜จ™ว ฆmžไ“$4NŸC$ฏ†\ฺคื7้MZo“ึ฿$ุ7iMŠผ›๛fํo‹Žk‹`ำgฤ้๘>z6'OะบA‚ซmtl”4ทุ๋ถk฿”9ท๋s้}จ๕eP‹xx‚€@gt™้[ีใ{๔๊จศl`_ ์{;งฅMxฝYxb˜๛vŸ๒ห“|R‚Lๆ๓[๕I๕Š 6๖+W‘๙<ุ้ฮvช1>๋ั็ฦDO๑eึ#๓08ฑ5ยฑูwvํŸ฿พา6ึ๛Q“ฐ" 3+€] `D๗4ญหH'ž8YPaชxํ“ หVŒ'์ Qภ]z}#า:ต๎FAธ1๛&ํg“ฌ}nฌ›ต-‚w‹> ่ใ"ะo@HVG๏วk]xmKu'^๛ฃด”8ฝื่A TQ}›ข๚Vฮ‰ข๚VพU ว)ช:ณC7 ๖อ‚ADu[}i#;ผฐฑํ›ำV mUvพEคR<ˆฎศฐจศ๐ž0มตฎiฺภWvพ&ุ=Av†•}Š/SDyฌOœโู$ซฅฯp๐๘๕Z ญ‡ืว๓oิ~ศ‚\@฿q#y‰ฑพ๓F5๖eƒข๚พAQ}ƒ@฿ ะื+ชฏ่h`_'ะื tฌKt่UหZ™ขz๑ย†–?ฏกํ›ูุ2&ถด??|aŽฮตรž6@1ี3nบแ†y‹ยดmฎํW~BXMฐSzข+#Yvžฦ NŽrKแ๒#ัยหt›๘ลฃๆดKi .8eวฮ้สƒ N›2!eCJˆ”ƒyไภŽmqะ#ส@zIXฬKaจž9+ิท)๙1าษจ'ษแVม‡• ยnึI<7J]‰( ๚Zค๕i›ตฺ6F๋ดฟhUรOc@Z?่ดภำฐฐJฒ/๋๚z}฿๕๚ิะืหพฌ่Œ(3_hฝข๚zพN ฏST_'ะื ๔ต}  Kบ์‹ษพT.m`G7ฐข‚}nC;ฃฑฅOlaฏ<~้มจ›9ผ"ใs LŒp[ฅOฃืฆ๊ฦใTพ๒ฐใอHHจฏ๒Eq๙๒x9nฮฅซ ,๙ญyt‰Le„Žฎ’หIL’z๒ท๊eฬ๎ีขศึใYu๑ใBข@ฃ|Huฅvม\ ๛#Ÿ*€]Jื๋iž YฬQa“F8)Q!Iฤ;S/ง”ธImI@๑ๅษศ‘[ƒดn mHWK๛[–^ าบAo }๛ฆืMZง๏ทNQ}์ห:}็uฑืึ tfฎ่kีื ๔5}@_#ะื๔ีา*ฎd4€\ะซzล’Vบฐฮo`g7ด=ำYฺ„ๆ๖๎“—ๅ_ฏ.ฦkํaุ}Bฝ1ืŽหŸ%รเกฯ~Zมฮ jฎŒชqSฎขR“ฅ#‹gP฿็@ฦ๏™ฯ$ฉ่o^œ[ตV‘nณ`฿&จS:Lจiิะ)/ ๖๊yใ€Q:ฐKiz/U๋คxๆจ$ xF4แคHU„ก-‚Mueƒ`$๙$๒ฅG๒rค๗W#ญ[-m[-ํหuT#ะzAa4}Ÿต}ญข๚Z}ฟต}ญ@_ฃจพF๖e@_#ะWหพฌ่k๚E๕ี}ต@_-ะW!พR ฃ‚]›ฌ‹ษงW ๔ฒE ฌD ฬm`น3ฺ๎),u\3๋๓๔‡N88`๗Š ฐ3!์ห ,…ง๚๛Wฒึ†ไฤoบฆึฮŒ8`๗Z;O๘ฅ"C’JFฯ/แฉพdžค~ฦทwj๖]ๆwฉฐ‚๊-Œ0R3งœ†=ˆๆ€ำO•๐RชKQtOึzษŠ๎‰ฺ&Aฝฃš ๋Sg0ˆ ฅC4ีทDa์ŠG๒rค๗W!ญ”ดj…_g]mH ฏ่ซ๕}V ๔ีŠ๊ซ๔V้{ฎ์ซีW+ชฏ’}Y%ุW)ชฏ์ซ๚JE๕U‚}ฅ`_)ะW๔@.-่@ฝJึฅB Y 3ฏšภฬh`ป&5ฒ”ฑอ์งฎ8|โ7ใผGรฮภื‡๋ฤlU,hMฃจ_ ุ้ฒ๘‚^kขz๙oวL9e]S’Jไยยำช}๛ทO9>cษ€ึลed“ ม’$8)%ทร)bืป”ฆฟำ๔zŠOŽŸ(เดvฟMภ3เCœz8ฅBHสˆxk,~ญŠG๑0ไ+]ฺฎF๙๛ฺfฅŽ}ฅข๚JมพRฐฏิ๗Y)ุW*ชฏTT_)ะW*ชฏPT_กจพR ฏTT_)ะW(ชฏ่ห๚rพB /่ห@_&ะ—"พ5ฐJY—2^2ฏพฮฉoy3๋ฉ l็',it3lLm‘= ปฯkvž#๓ต‰์ผ'ฉ5•ษิy2“†ข“ิ๐ด|ป?e€฿Ž๚ˆฌฬE>F3laไ“y$”ƒ[แ;‰(–ลaO4{๘ฝ—>Q๋'๘ํฺv›€฿*เใดฏ-~“€งTHๅ„„’ไฏq‹R yๆ_$ญ‹– ๖ๅ‚}นŽน`_ฎจพB฿g…พ }ทๅ‚}น`_ฎจพ\ฐ/์ห๛2E๕e‚}นข๚2มพLฐ/์ห๛RพT /A=€\Z$๛"ะหe]Ž๔"~X ็Nซo{'7ฐฌ๑,แฃf๖ส//ษฏw\เูฟvl žk๙ต๑์ัฐ‡ห^‘กHE&œค๚ดJXัพบnƒKจีงg๏žyU…mิ ๕dPˆแ('7ED`ฌ‹ร.ฅIฉ๚;Uฏ'G€Oิบ ~ปถ‹๐[ต8ฟE=ล&ํsฃ€฿ เ)ฎ“H0I8๑฿nOช!ื๛hy”–!ฝ_-มพLฐ/์ห๛2}‡e‚}™พฯRมพTฝึ2Y˜e‚}™`_* ณTf‰ข๚มพDQ}‰`_*ุ—๔%}1์‹๚"Ž tฉRัผ\ถๅศ๚V2[Q] ็Oฏo9S๊๎OXฦุFถํรf๖ƒฌS'จฦ„ง 8์ั *ีิฏE5†ลaวส{tEฦ“Tบ9OR}ฺ' ฿\"zิv#GทN8.}าk- lภ`t“!uF?น.ธ Nœ๒"‰h{๔๖๐ษเต^‚ึ฿.เใตํV'เท๘M~ฃขzAฟN๛_+่ืHซ๏ชˆVJGAฎ๗ธ๙)-ี๏ี่tผK๛RมพDฐ/ีwX*ุ—(ฒ/์K๛bมพXฐ/VT_,ุ ๖ล‚}ฑ`_,ุ ๖ล‚}‘`_$ะ ๔…Hฐ/่ ค๙๒้๓Z…ัฒ9ฒ/ฝH ่๚พ‰๕-{\Kีศถ mnOไ<๊์ัฐ‡KŸ7อ๗+ใu๖pEฦ“TN@๘1x>m€s Jเ™EGํ6<ทึz;zเบs๖-ฝถ*ีdค“‘OFA™ไ=๐่X@ูงJ•R๔wJ๘DŸ uทk›m~ซ"|œ๖ฑYภo๐ตฯ๕~€_+เืHซ๕Yซค•าŠˆศ‘_Š๔;Z–€G‹bฟXวฝXฐ/์‹๛b5ล‚}‘`_$ุษฏ/์‹๛"มพPฐ/์ ๛Bมพ@ฐ/์ ๛Žๆ ๖y]ชœ+ะ็4ด2%ขฅณX๑Œ๚V ะ๓zฮค๚ถg|}หำภ’G4ฒ๕›ู]Wpฮo๖๐ ’฿xํdny—GT๚JŽ ๚†/G‹ๆหึ”คrฟข?พฏวŠ๐ณฺ™ษdฃ๐คฐh+ƒ3ฟ™น~x‡า &อ\r&M1สh(๗{RVค๊R {๔v)Yภ'้ฝDญ“ เทkm~ซถ๐›&ํkƒz‹๕‚~ _ซฏ๔ซฅUาJAฝBZ.๙2ฝถ4ข%Hฏ-Kภ/าq.์‹๛"มพHวพPฐ/ิ๗XจฦปPฐ/์ ๛BE๖…ฒ1 ๛ม>_ฐ/์๓๛|ม>Oฐฯ์๓คน~ฎ`Ÿุชๆ4ถสูฌbถ@ŸีะJg ๔้ ฌP@ฯUD฿7AQ}l}Kจmึุ–๔jVูฑ๑;t~9็^gwุ้`<ฎ0H ^ˆมรฏ์_€w฿^S’สษ`3Q฿N๗ฎท3“Ž๔ภœ้/ด2่/]š[IษŽกuF;™-ศฮม ฮ‚•๒bช ฦฒ๖ะ๛T)RฒNา๋‰z?AภoืบV'เ7k›dk6๘๕ฺ็:ฟFภฏ–V ๚•า iนไห๔sฉ@.-Žh‘Kฐ/D~ก€_จใ] เ๘Š๎ ๔=ๆซ๑.PtŸฏ|dพิy‚}พข๛Tท‘ำฏ์Mื,5มž6เ#ฉa฿ฮำม๐ํL ร๗qโjฒ2แ›9>“จ^qฉ;Rวw- yๅ*p ๎่ค”ฉบ )ฐ‡@`—’|ข” ๗ท ๘mZซ€SCู,เ7 ๘ ฺืzE๙u‚~ _ญฏ’V ๚าri™ดTZ"-ŽhQD ี ๘Hวธ@ภฯ๐๓<ม>Ouž๎<?Wั}žข๛\E๗9Š๎s๛\ม>Gฐฯ์sg ๖ู‚}–ข๛,?KภฯljURๅŒ&V>ฝฑ•ModฅำY๑”†V8นžิภ๒”Œ่{zถ@Oัภ?hd๛7ณAO7(>ํคใฑŒ{x"ฝญ฿tM`โึJmP๘ _ู๙์แลa๖ํ>’๊พ็ถSชขN–ฎ+ร\ ็ฬoWej`โข๖๚mฃผ อy๔]=K ง mh5ถ5๏7ท฿฿y๓bศ‘ยฐ‡็ณS-cJ6A ?๓‘ฒ2ำนฟฒw*…—hุk๒ํ>ทรค0๎p [™๐hj๘๑แฆฯ$ชญฏ๘vv๖ิ๎ๅม`ฯ&Aร0?ฃ ŒˆR?Oฐ$กAB ์๚/>U’~O”vฝฟM๋m๐qฺfณ€฿จํ7๘๕ฺืZAฟFะฏ’V ๚๚Œๅา2i ๘‹ฅEาBiAD๓<š'เั\฿\?Gภฯ๐ณgุ๋gซมฮ์ณg)บฯ๐3 E๗™Š๎3ง+บO๐ำง ๘iญฌjjKซœฺย*ฆ4ท๒ษอ์ศไฆV:ฉ‰Olb…Ÿ4ถรใYธF–;ถกํะvหฃ๏๘ฐฅ ๔ฤAls฿ฆถ่อ–U7ถ๙7\{ง๘;ื)ุฯZ=ๅ‚ฃโ พฒ๗ †พํ}บ/๘vN V†ฉDzœDฆ๚“฿ชWkขZ๏ธ:้ƒะMภO๐Sงดตช)ญญrr++Ÿิาส&ถฐ#›[ษ'อฌxBS+฿ฤkb๙c[๎˜FถT#3ฒ‘ํะ2†6ฒdพญ_[ืซ…Mxฎน๚ษUั}Šข๛E๗ษ~ฒ์ฬค+ญjR;ซ˜ุฦส?imeZY้๘–V2พ…kn…7ณรc›Z่ฆ–;ช‰ๅŒll{G4ถ์a-๓ƒฦ–*๋ฒ] o๎ฬVพีฺ^~ฐแŽ?Žs๋ฐ‡k์แน์_ป7jZ๖ฐož†•๑ฉneจำ๚ำ%ๆสxขJฉ‹D•<*5Fwข4๐ู๙ถZฏ8 ๑ว ฆm‚Œ"j่”IF๑่ADwุื๛าv/mำ{[ญทY๋o๐ิPึi๛ต‚~ต๖ตJะฏ– ๚ฅฺiฑดHZ ๐็K๓คนHภฯ‘fKณt<ณL?Sภฯะ1N๐ำ4๗45ึฉ~ชข๛E๗)Š๎“'+บORtŸ(เ'vถชO:Zๅ„V1แJ+฿ฮสฦทฑาqmฌไใVV4ถฅŒia‡FทฐผQอ-wd3หัิ๖ojป†6ฑฌ!M,m`K์ืิถ ๔uoทด/ทญู๊๎l๊๋‡ฝถJ ฃ\'žAฐ"p๙-yD0ฎWฎ^๖pt— รV&\•กNหK$ชฑNTฟ่-“ขหGE๗V—Ÿ‘ฝcโ ๅม€ฯมณY 1 บMภm˜”.ษh`]" Wร.ลK๔๚VฝฟEภoึบmทNภฏั>VK+ AฟLZช}/– …าiพ ŸัiถŽa–4Sภฯ๐hบŽmบ€Ÿฆใœ*เง๊ธง๘ษ~ฒข๛$?Qั}ข€Dภ"เ'\mUใปXๅ๘ฮV1ฎฃ•}มŽ||ฅ•ŽmgลcฺZั่6V0ชต๚จ•ๅliนถฐœแอm๏ะๆถkH31ธ™ฅhjษ}›Y|๏ๆถ๑–ถ๒๕ถ๖ัSญŽœ๗“8แไุษ—ยษi๘Y(Jศ…๖ตฉฤ๘ยsเvพธ[Ÿ฿๎V†r•O ๓D•‰E>}€ษFดฐpฒฦ่~\:io>ั4ฏŠRเ:มรจ็ลh่6มH ๒"‰(aOxB๏E/mำkqาฝฟY๋m๔๋ตอ:AฟFฏ๔+rํo™ดDภ/–J ๔9๓ฅyา\A?Gš-อ๐3ฅ:–้M๐Su|S๋d5ะI~ข์ฬDE๗Oู™ ~ผ์ฬธkฌj\Wซ๘jซลสฦvถ#c:Z้่V<๊J+๚จŒlk‡Fดฑผ[[๎๐ึ–3ด•ํ ฅํยvla[XJŸถ]}หญlํ๋ml๑‹์ฉ›.ฯำ๙ใœFWb8็ฬBลN๚ิ^žไๆษ)ฃแ_ซไ4ผ8์5Yช2>ภžฎน3์3!๑†^†$บS†ฤC†หGE๗หฯ;uGยG=ŽupF<"ฐ$ข~/PIBq‚`"บƒ+ญ#mี๏qาฝทI๋l”ึkต‚~ถ]%่W๚ๅฺืRAฟDZค}/”ๆ ๘yา\iŽ€Ÿ-อ’f๊๓gHำ4?Ušขใšฌใ›,K3Iว:Q ๔‰ข๛?^ภSt';๓๑uV5๖ZซsUŒ้jeฃฏถ#ฃบXษGญxdG+ัม >lo‡†_iyรฺY๎ะถ–๓A7ธํฺุvhe™}[[๊๛ญ,๑ฝVถ๕ํึถA}ลKํmา๏Wดธ๐ฬ,?ท0ภ๎~๛H !เx|šŸ&@เ๚J?๚ฎถฅฆ่ฎส„˜˜+ใ5wFแ˜=‡๔2$ัHย– ๘ฅาbํs‘€_ อ—ๆ ๚9าl},i†4]ภO“ฆ๊8ฆ๘)‚}ฒŽk’ข๛Dใ'~‚ข๛x?^ั}œ€Xvfฌข๛˜ฌjLซ}UŒบึส>๊fGFvต’W[๑‡]ฌpxg+ึษ ๋hytฐ!ํ-g๐•ถo`;= ญํ์ืึ2๛ดฑด^m,้6ถํญถถ้ตvถ๚ๅถ่๏]์๏ท7๑ฤิฃบ'งุF์cx0‰โs™ย~ง ิ๔฿ฏ4์,แ่ฮ๗ชLm‰*ั ๎^†dพ4ั“kญฬ|๏'f.๊}mฑญPkB mtิฮ)-Rq!ลฃบ'm‘6๋ตMาฟ^๋ญี๚ซฅU‚~…ถ_&่—h_‹ฅ…‚~ Ÿ'อีgฬ–f ๘™าtiš>{ช4EภOึฑL๐u\Ÿ่๘&๘๑~œข๛8?Vั}ฌ`ฃ่>๚fซu“U~tƒUŒผสFtท##ฎต’ปY๑๐ฎV8์j+ฺล}ะู๒†tฒA-g`Gืฟƒํ๎ืv๖นาฒz_ii๏]iษo_i๑o\i›_mok_๊hหžฟส&=uUๅ•—žลsb8—Ÿga˜ย~์7฿0{ีAŒ1•๐`๚ส/aุ‰๎tk5%ชd๐L๘็9x?๎”!๑„>ศไั=์kญฬ ›ฺณ๛ภŒป+‚แZล ะ&มถEฦ ฬญ•ช |›`์  ๆSm‘6๋๏าฝทNZซ๕Vk•~นถ]*เ—h?‹ค…~พ4W๛ž#เgI3๕YำฅiาT?EŸ?Yš(เ?ั๑Lะq๐ใวcE๗1~ดข๛hE๗Q?ฑชnฑส‘7[ลˆญ์รvd๘๕V2ฌปฝึ ?่fCบฺกมW[ ซ,w`ห้฿ู๖๕๋dป๛tฒ์-ซWGKงƒ%ฟีมถฟัโ^้h^์l+~ต-๘๓ต๖\ฯๆDuฮ G[/9๚Lว่วg„๋๋~รํkaa|แK:๐แD•ไ%z๚€?@ษ'‡แ}พŒ2นwฏฉ2รE๚Ltง๙“ํ๒ซH W .„ขfNํœฒbœ€*x๑่ัซA—6Iฅ๕z}ดFภฏาบ+ดอriฉ _ฌ},๔ ดฟyา?[๛Ÿ)อ๐ำคฉ๚ฬ)า$หDร'าฯx?Nภ๐c๛hE๗Q‚#E๗‘Š๎#nตส{Zล๐[ฌl๘MvdุV:ด‡pฝ้nƒฎตCฏฑผ,ทWห้{ตํ๋s•ํ้ลฒ{uฑ๏vถ๔ท;[ส-แีฮถ๕ฅ.ถ๑…ซl๕฿บู’?_gcฟฆผ๑น฿sฏ๎ฐ‡ซ0œgฮ7ฝ*7ึ๘#๏|๒—Ÿ้่๕๕ฏ…๑%้ึยำ<บ๛3eผ ้ั™ˆ๎<"/\™!ส๘m{ตŽชข‹~tjึ๚ม=Km…€[-ะจ“oจ›S?฿"PIB๑ๆ[2 ?)ะฅาi^[+ญ๐ซด ญฟLZขm ๘…ฺฯ|?Wš-เgi฿3คi๚œฉ๚์)า$}๎Di‚Žcผ`'ุ?์c๛ม>Jั#‘ข๛HมแํV56ซ๖+ฺำŽ|pณ•นษŠ฿`EƒzXมภ๋ํะ€๋,ฏ฿ต–๗ห้sํ๋อ๖ผืีฒ฿นฺvผuตeผy•ฅพv•%พr•m{๑j๔|7[๛ืkm๙ŸzุงoถŸuj”{\:Špb๊ฃฆต™ไ๗œ๚์hmๆk{mัฝฆUb˜Gwž>@wษฬ:Nrธ๎އฌiฮฬg€ฟญใ…{rง?T$”ซ฿Zท^?7 สM‚t“€,p๑็ุ– ข‡@_์า`—VjๅZwฉดX-า๖ <ํkฐK3ต๏้๚ผฉา}ึ$}ๆDi‚`/ุวIc?Zภ๐#๛มกข๛๐;ญjุV1๔v+เV+๒+|‹บูŠdoดCzX^฿๋-ทฯu–ำปปํ{๏Z๓๎ตถ๋ํklว›,ใ๕n–๚j7Kz้‹แ๗๎ถ๎น๋mลoฒ…ฯฤzต๘ฌSOๆ(๖pbJnฤ@ฯv๔ฤ”$๎?๐๙๋ไ\\รฏ์=งฬk+C†ฃ;รง„+3ฬฒ#บ๘จช?L)zFไQvฆquำ฿yปƒๅ Q$•ซโZiดA€nจํFมปI c[€<ทŸ‚พVZ-ญา{+คeZฯa_(อ๐sตŸฺู฿L}ฦ iš๖?EฐO–& ๘ ‚}ผ`X+ุว๖Q‚#ม>RฐPt.เ‡cUC๏ฒส๎ด๒!ท[ูเฌtะญV2 ง๕ฟล ๚l‡๛d๙๏฿h{฿`๋a๛฿ฝ๖ผ}ฝํz๓:๑zwห|ตปฅฝ’qm:๒ืถแฯ7ฺช?bKžพฦ>ึณขํ็๐Ÿ๑๔pb๊saย‰ฉืึน_ุS >Tำจ้ืv๎žจ~^tง2Ct็ฤ2‚บ;w21Lอž฿A7๛…ฅศ๏๛[™ณ^ปฅะ–ะ‚’jส‰Rโ:บ^ภ’„nฤ๋3ั<ˆ่‚}ญ€.ญ”–๋ฅาญปH,ะถ๓ด9ฺ๏,i†๖9MฐO์“ฅ‰‚}‚`/ุ?–ฦ ๖ั‚}”`Hฐ>์ร๛Pมม=V9ไnซ|ง• บรŽ ผJ๚฿fEnตยพ=ํ๐๛ทX~๏›ํเ{7ูwoฒo฿h{฿ผมvฝqƒํ|ญ‡eพารา^บม’_ธมถํF‹๛หMถ๑ทุšgoตeOe3Ÿธง๊พึMs๊ึ =:ช‡็ฎ3= ฤ^ฎื…kNLนžัI_Kุรภ{t๗2dMั=\™aFฃช>g†ฒ7๚rจ๛2„ฝLข\;ำ’๏๏Œ๚ภ["Pฑ!+(••5U–ตz$๎ั|ดZฐฏ’ะฅe๛Xคuj›๙า\m?[๛™)ุง ๖ฉ‚=]๚Dฐ์Kc๛hม>Jฐ์#ค๛0E๗ก~ศ}V5๘ซtท•ผหŽ๔ฟำJ๛aล}oทย๗oณรฝoตCฝ~by๏๖ดo฿b๛฿บู๖พqณํ~ํ&๙สM–๕าM–›-ๅ๙›-แฏ7ึ?๗ดMธีึ>s‡ญ๘ฝ67ูบu>tา7ฟษ๙ฉ)ช{นัg8†หฬ]'๘๘๔ฟ+้kY[ฏm รNt—!รัo์ื™หMูฬ™กถหศ/E’ฌบแBีjgPฯ+/ณ{/สmษ๏ญ€]๔า๊ˆึ`|yเฯ5ัผ:ขK่าbฝฟP๋ฮ—ๆjป9‚}–`Ÿ!ุง ๖)า$‰`/ุ?–ฦ ๖ั‚#ม>R๚Pภ์C๛‚}ศV5่>ซxฏ•ธวส๚mฅ}๏ฒโ>wZQ๏;ฌ ืํv่,๏[-๗ญŸXฮ=m฿k=m๗ซ=-๛ๅž–๕bOKกงฅํ'–๘ญถํOทูๆ฿฿a๋ŸพวV๖๒Oํ[n*:๋ิS™ยฃบO๚๒จ๎ฯa็^SŠ5EuฦLขฃ๚ืvุk‹๎แสŒO๕93๚ๅฅHš่V)…๙`ฅ?‰ V;ร]M฿ุ|ม)OVฺโงeGฑฑ%๘๐•แห€ฏˆ่3 K ๚<)]š!ุง ๖)Rบ4^ฐ Kฃ๛G‚=].ุ‡ ๖๛`iะ~ซ่Ÿ@ฟืŽ๔นวJฟŠze๏i‡฿นร๒฿พพyปๅผ~›ํ{๕6๓๒ญ–โญ–๕ยญ–๑๗,๕ฏท[าŸ๏ฐ๘?e[žฝว65€จฮh5‚ ฅ_๗๊Duๆ3๙ุ ชmหต-ััฝ6๏๎O บ๛œŸhb 0v†ป‰@_ฺฮ_๏ธ๔็๎n `ฺณ•ถ่๗8=0uXผ†–J€พH: tE๕Yา ม>Mฐ +ชO่คq‚}ฌ4Zฐ$ุG ๖ฅa‚}จ`"ุKดช๔๛ญผ๏}vไ๛ฌค๗ฝV๋+|๗n;๖]–ๆv๐;ํภkwุW๎ฐ=/aปq‡ํxห–๖—ป,๙Ow๖?kqฯoŸzศึ๎QตีŸุ‡(kq๎y$ค^fฌ)ช๛ญw^Wง๒ๅzVŸทNTg@0ีkYj‹๎^™ฉiTีŸ ษ|wF๋ย“ฤลใ฿ำิdgผ:S+๐'~ฃ^๚‹\•[8๕•ถเO‚๘YElAฝ|ฎล!ะ็K่’ƒ>] O•& ๖‰‚=]ช])ุ?”† ๖ก‚}ˆ4Xภ์ฒส~ZyŸฌ์๛ญดืV}V๘ฮฝV๐ึฝv่{,๏๕ปํภซw—๏ถฝ/mป^ธvnห๋=–๖ฝ–๒ว๛,แ๗ุถgฒอO?ขจจญ~๊gjU๕lาpo ๊้a๛โQGK™ณ=ZสTkชa›p†k‹๊ŸณิรuwFUรsfฐ—"™F@ฒสญ`a;ใี?่‚}ฐ‰ ษ… ๛๗jเฟ๕ใำŸฟ๗ชC“\i๓*๘)๔‹bAฟ ๚…H /}ฎ4Gš%ุg๖้‚}ช4YฐO์๛8iฌ`#ุGI#{บ4Tฐ‘ ๖R‡ญช฿CVั็!+๏ ้๕€•ผ๛€ฝ}ฟ@ฟ_ ฿gyฏgนฏg9/k๛qฏํ~^หฝ–๕—{-ใฯ๗Yช@O๚รถูm๋ณู&iณ๗Šg๏ฒ7n๏Tx๒ ว๓ฃํKMHž”๒8JฝŒq0ZสL ืีkšสƒ=ด„ฃ;ภ‡ฃปชRณ%บ“้{)า“ีฯณ3=ร`บมฆš๛gึŽฏ—ป[ฺ็ไŒ}ฎยๆผ ˆœ€ƒภ๔ ‚ง๔ท Ÿ'เ็Jsl@—f๖i‚}ช4YฐO์๛8iฌ`#ุGI#๛‡าp?T"เ ๖RGฌช๏#Vู็aฐ•ฝ๗•พ๛ฟ พ๙ ~ห๕;๘๒vเล๛๚ถ็๙๛lื฿๎ณนฯ2ัำคจ.% ๘ํผW‰้]ถ๙ฯ?ฑ๕ฯ๕ฐE์Vusณs๙HัQฝ&๛‚๔™”xIJิฃ"žณ๎Qk_WขลaGแ่Nฒส $b0ฯย็ป‡“UžMโv†สฅ0๎ƒd*ƒM›šh๐\Xจ่ŽŽซ[7ํžNM๗ฆ ๙}™อzY@]` ๚นŠ๔sg๔š Ÿ-่g ๘™าtA?MภO๐“๛Di‚€'เ?์cคQ~ค`! ์Cฅ!‚}ฐ4Pภ๗—๚ิช๚ิ*z?"ะฑ#๏lฏ?l‡^}ศ๒^~H ?d๛_xะ๖>€ํ–W`ฟGฐ฿mJ@,๕/wZ ษ้฿n ‘ถพp•mxพฝM๚]ซ๒ พ{ๆ๑‹์ ะ็ซs;$ฝฆ'ฅุH’RŠ~ฉGuฎกƒƒ=j๑“Ž๎แRคฯw๗R$ษ*SH=Y๕Y‘^๑ฉ$Rิƒฟ3ฬอE 'ฌัภ}ปหฮฯ^๐าใEๅS^ซฒ™/ lA?Kะฯ๔3•ศฮ๔3ํง ฉ~Š4Iะ"่'๚qc?F%่G ๘าpA?T"่K}฿๏ggVู๛Q+๏Qจ@ิŠ฿xิ ^๛ฉ@ฉ@ฉๅพ๘ˆxAฐ?/ุ~ฟ`วยศฏํ.ห๚ป’ำ็oณŒb้/๖ฐิ—ปZา+-•6ถ๑ฅ–ึง๕‹O>ฑ฿9:ชื4…—ส7น3ห›ศ่5ฝ)=+๗ะใb5้…cQK.aเ=บ%ยฅศpฒส‰&ฒpาI’ธLDยKr Oฃ…‘R~#ฝ%„ BพฤdTษ(5’[a; Paะcฐืฒ„a็„ผG๗š’ีฐ๑๊ ƒ 6E๛w*”ฬฦI$ฌ4๐วw\๚uM์žยoŠJ>~ซส&ฝ)ธeo&?ฏHฎDvขข'Š๖xฑภ+๐G Qค4B๐รา`ม?H ะO๊๓„U๕~\ ?nๅ๏O ๔'ญ๒ฝ฿X๙ปฟฑ#o?iฅo>iลฏ?i…ฏ>ฉจคข๚ฏ-เ_™ฌฬร–ง$•ŠL%H๊ํofนo๗ด๏h๛฿๋n{{]mป{wดฝZf๏––^3[๛jณช›[}Ÿง{9์ž”ีฑwไ5>ซ‘๓ๅ๓_จฉLธ1ร“FุพฤJยโฐ;๐แd5ฺฮ๘ชTgฟSฃซxบ]ฏฟใ9IX๊Ž“V"|n”ฏSงNฺู฿๙Nๆ“ืuหY๙?–๔cz pE๛1ฏr?Jฃฟ tY,ะPมเฌศ?H FะO๊#ฝŒU๖zฦ*}ฺส~F ?ฃจฌ์หณ‚E๖งค฿ุแืท‚7~f…JV ฉฮผsฟพ{ฏ๔บห๕บ๒{๗ดƒ๏฿d๏n๛๛\m{๛tด์>‚Oa_๒|ำŠfžVำŸีIJ้1ู>RJoหu‰ู—q๑“ล‰ใ†“ีฐ๑๊ ีlb~เ™ Œ๗๚;!ฬŸ ฅaภสฌ>€'ส…mMญQ่ฟ{ส)™wถiณwฬฟ8œ๕๋ๅeร฿ฏฒฤ[‚5A.ซ3L๐}A +๒Vค่?@ Ÿz€พา๛ฯ ๔็๚sฒ/ฯษพ<'ุ"ฏY๔'+zใ+~๋7V๒ฮ/ญT‰kiฏ‡ญ๔‡ฌดฯVา็>+๊s๖ฝำ๗ฝี๒๛l๛^g9}ปฺพ>lWŸv–ีปตlLK›๔tำ#฿?พ‡Guฏซ{ฦฝบฯjไ_๋ำK2RJ~Do๊5๕่‘า่ย>ฺฮ„ซ3xF’$lbf$นLฃN๕€ู‘ัภำESi ดฦฆ๛ฆถL„s[S[”? z๔z๕าœ}๖Ž_uํถยฟ*H{ํตฒย๏WV ้c6D๐yG+๒|]ษจlO?๕}_ถช๗_่/หงฟ,ะ_VTYI้หV๒ึ+าKV๒๖๓V๚๎ŸฌฌืSVฏฌขฏ’ื~ZEŸZล€‡ญ|ภCvdภƒVฺ>+๎ท๖ฟ๗ปล๒๚๕ฐ}ฏ์]lื๛,๓ฝvถ+ํ•;ฏ8tqu๙Nีฝฎ๎=bZน H~5e]ข:=(๖16ซ๑฿ผ8์žฌFWg|*Aด๗ฉภ$Rแ„• ฅมร“ดา]sำ™.่!+๘€•ธืŠ๚฿iˆ๎ฒ2}บพ]-๛ฝ.–๖Vg[๗๗ซซz4>{—Ž1:ช3Zส๗ฦึ…Ÿพ‰บถYž”ฦ@_,แ่ง๘`Q๎7zเ)=a๕'#๐๛ซn:๐T็NฏกMz฿qฒg~_’๛๎+••ƒd†ศ๏UB;Lภ๛ฅ$่‡ ๚O?2๐ บ๔“w๏s‹่}ƒํy๗:๑fwKz๙๘@็ยำฟฬYGuu๘ฦ ฮAภoขf*‰?ŸซŽ•ŒE๕๓ |ุฟYj๒๏แ„5x,'ญTi(KโKxขžL๗Mdcด•(๏Pัะ‡ํอ็๏๚L๘"ีญS7ํฬ“Nษธนqำ=~๑hA~Ÿ*m˜‡OIฟ–W>๐)๐Uศา zXvๆ+๎งdตฯ–ื๋VNO๕ฦ-–๖าํ6๏ษหZ๘์pb๊QSฆSะุKvฟ‰š2/็˜๓อน'่„ฝzl๙7,; ™hN=œฐFฅมรฉžฒ$ษOD1ท5Œถz”gธ๋ึ†๎@ˆˆ๎้‘G๛/ ดju๒ 'ค฿ชๅ•Mษ‘!ญฒŸ์ฟ์ N\‘Q+๘ˆ|๛ƒJT๏ทCฝ๎ฑo฿eป_ป2๑อีƒGบ_~Yvฉพๅ.ี๙^>5เ๓ ˜?z๛ˆ•ไ:ฤ,ฬฟy‰Ž๎ั๋๏>€งฺ€'iฅJใuxžxd๗ฑbkxGy"Ÿ!s@ ๒=]>ะ %;?๑?บx#จI_ ~สฯ฿#7?”U|๐œU }J Jๆvคฯฌ๘Ÿฺแ๗ฑ7ฑ=ฏตj‡˜ผx€h ผQxCรนเืซ{\๚E฿^ึ-[ํ๛๕ื|ซgฯ{”๗XN๛[Ÿ{มŽSN๘{ฃrะว๖๊ไ&ีiเ>แ‹๓ย;Ÿ€M๔AคXb๚XvDd 'ฌDœhเ๑๐\(ช4ิแxยึpakˆ๒Œถz”'šๅ๑๒XบsXผGzJ•žFดƒOฤว฿)ฐDODC๐ฦh( ผx"๐ร๐๛฿ฌ‡ุฦ!wyD—eฝ•{u๊๔vุ=์ŸฯW็œbร‰i ๖ร%ฟx<Q@kฒ:a๘๏0เ๎อรณฟ0่|6วฦ1ำk1๗Ÿปฐtไ4ไ8DuฮQs๊Q=faƒห็๏ž.IREมm #แ(—gt/๏ึ& =‘{Cฉ’ส @pcC้>ŸศH%รแ'๒SฦคPู*ๆใ J›๘o4า ๙‰๘ุV? ฟ๛pแF๔NoAใกั˜่]ฐ]€ŽOงสw 1“ท๘]H๗๊~RtTมZพxฏรS*s[ร… Gyฆฝ€žZ2 lุฯ=๖†R%žžจว@ ัม๗ˆO้’J๏Kไgp†F@@C ูฎpƒ๐>0รษ/เ‡“_;ุทNฃaDs๖ษพi`|&วBน•ฦห7“่ศkH์ฑ€>7ี/ัภ{•ฦ๋๐aOt๒(ๅBR;vkใะใSํ Ÿ$ฒD=,Oฌeฮเ ี ’Z‡x(cRีมSแ ภ*เ‰ค จ๋c#˜˜ฦด ฤ$U|ตƒ๏ษ/#ห_#`rว"ฑ=ถ‰„š}ำใ๐™วJใe†(฿“\ฦํ ็ส+0ฑจ_ฐิ<เรถ&ๅหปต! ๛yn๗s{ƒweฦ๓Cˆ๖L= R๘D|ฆ เ๏•~ๆ~๗ม๑๛˜ข@ตใy‰แxฌ ”:้˜^ ^๎ฤr>Qู“_J†ัjCXึg;๛มNฑozz!Ž…ฦสwt8๗๖r><)%XxT๗s๑†ฏ)สป—็&ฏุDC๏๖† wu‹ พG|ฌŽรฯˆ#‘หC๔ว๖ะเ๙ฉ๐xc@T=h T|€ŽK/@๔|€ฤ๖x๒ ๘De*?ฬ฿๑ไ7Zผฮ๛ฌ‡5b;ถrzl = ๋โ 3แทั rฎ8oั๖%๛ม> }8สใๅฺิ=๖ฦYผ+เเcs<โ“ิ2* Tsˆ”1iุ&Qแ๙Ivฉๅำะˆคh?ฝƒZD}r">๗ฯ:๘x}jแไ—ฒ!›ŸˆืฐCฌว๚ุz , ‰†ลg๑ู๋โ sงsฎb๖ๅฟxฉ ๘่(๏ึ&z์ ‘yŒยบฏw๐™Gฤว๊0บH2CrK ๚ใ๗๕j€Bฬอ๑ฦ@ol4ฒW} ไ5~ผ?เ๑ฝ๒N~‰ึˆ฿ฏ๓>ฝษ2น๛ ฑ_>ƒGy‘ใฃJcv๋่œ=f_ށๅ_…Ÿ๊‰,ั“YŸ่ว@ > ?ษญ7ข?(ยฝ๐ผ๊ใ9P†“_>๐๕ฉx๒๋โoฤ{ฌC'Oภ*ฑr ๖M#ฃแa[ศK(ม๒ฝบ8่œŸhะcฐ/5mmzท7t฿$ฒTo<ฺ‡มงt†ปC ๒ใ๕รภ}?ฦ€ 0r๏ ล%ย>‘๐=โ‡มว๏S๋ว‚x๒‹˜ฯรOฏ“ำ;เวษศศ%ฐU@ฮ็ั9.-ถ๏ฦwŽ่œซ่วุR๔้๑๔แh฿ซ8Œ&bu~์Ž7ภกdG#ภxCpDC ’ึ–"5~ ๔ˆ๏wฟ๏^ั\X ^รaQ€›ž‚ํi@๔"๔*๔24:#ววฑำ˜๙n|_พป{๔่ว๐โ-z฿กŽ๖a๐๊„แ€Gฎhˆd ผ!ิd…<๚c<โ{นำ=?‰/๐ใ๗ฉฑ$$ $ส:ฌห6l เX(z>ศi€ Bc๖๒b,ขล–š w๐ระGƒ๏0ย๐‹7ฌ"‚๗แ€@O๐eซ>^๒\"?ฯฯOฤkˆ๗Ymh44๖…bDr ็8.ซW\๘Ž|฿่ˆ[พ"หA_๘nuย‘Ÿnยธha+ไ9@8โปRธ๋ป฿งธ๘จy๕ˆ4ถง็ !aซ๘ wหยqqผ|ๆ|_?1ะฟขK๚ฯฟ6๘]แFnัฝApี‡ˆ๘โ๕6œ๘"|?โwฃฐ.ฐ-ฝ€cฉุฟCฮgsแhถ-(ถ| –h่kฟ&๘ร ยมBt|nˆ๘แชOดื'B‡ลkผ‡X๕ูŽํp ‹H๎ป7็8๙ฝ;ว–ฏู๓€ซฆ†เ เยเปq๐ิ“^์"J‡ิผฮ:ฌ๋pมœั ่U๘,ท,วล1๒๛ล–ุr๐aEƒญšB~ฟฆˆž๔†฿h6๏;มร€‡#9Ÿฯ1๙1๚w‰-ฑฅฦ% |m CVธ„มwปS›ื๗Fเ๒ืlึ รอพขฃธถ‹kl‰-ิ๓ ็E|ไ^ฟ6๑พร x๒๐็๚ฑฤ–ุ๒o_ยฐ‡0 ~4แFเŠ~/ ท^ไ(ถฤ–ศ†…Dj/RMpฃ่ฯŠ-ฑๅู #Š๖ŸUM๛Œ-ฑๅฟjฉ าh}ะัŠ-ฑๅ˜Yjธ6ล–ุ[bKl‰-ฑ%ถฤ–ุ[bKl‰-ฑ%ถฤ–ุ[bKl‰-ฑ%ถฤ–ุ[bKl‰-ฑ%ถฤ–ุ[bKl‰-ฑ%ถฤ–ุ[bหv๙Ÿ๙y'!Œw๐IENDฎB`‚lemonade-sdk-lemonade-dbde812/docs/assets/logo_512.png000066400000000000000000002417431516551144000226360ustar00rootroot00000000000000‰PNG  IHDR๔xิ๚ cHRMz&€„๚€่u0๊`:˜pœบQ<bKGD ฝง“ pHYsรรวoจdtIME๊—ห%%tEXtdate:create2026-01-06T00:52:08+00:00ผS็ึ%tEXtdate:modify2026-01-06T00:52:08+00:00อ_j(tEXtdate:timestamp2026-01-22T18:03:28+00:00ชP๏€IDATxฺ์็šไ8ฎ.Œ$%…IŸeฺ๗t[๛<็|{ŸฝfึLO›2้ยHฤ๗Cข‚ข’Šฬ๊ฎช๊ษ ‰N$%/^ภ,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณฬ2ห,ณ|Xม?บณฬ‘ฯ๕๙ค?บณฬ2ห,๊ฎภ,ณ%mก๛ f™e–ษ2€YžRžJ 3 ๐cว*:"ฯฑuœe–Yžนฬ`–วศSXๅS•}N๘๏๕\Sf\JAฃ๘ง–9ห,ณฬ2yเ,วสc•J‘O„๒๗ฏ๕”สึอxl™๓„รYf™%*30หฑrŒ‚ว iย_‚88๘ฃ@@Š๎ฯq๐›บS0ณฬ2ห3•ฬ’#ตๆcaœโN)๚”ฒ?fALr26ๅ˜Ž(๓ุzอ2ห,ฯ\f0KŽ<…ต๏ยR ๏&8Vฆ(ี˜R—”l.@ชœวึw–Yfyf2€Yb"Yฺ”‘F:Ÿขุ%k๐!@Ž๒)nŠคOฅง๊>ƒ€Yf™eณŒไX๋9Fๅ‡aนแ ็๒ฤ๊rL›|™ขxCลS๘28Hไถ&ไ›e–Y>3™WฬโหTฅ(Yูฑ฿)aI—ใNHต)ทฝนพ{?<ค0Ÿษฯอ๊อ๔Ÿขิ็ณฬ๒ eฯ[ŽตŽc4~๘+)ํ\Ÿ“/ฌห1sR’ky็*y), r๋*-QD&m,|–Yf๙Lev<_y ๅ‰ธ”BOว€๋X9†ฮwฟ6ˆKไ?,kSn๘,ณฬ๒ศฬwš๎S•\ๅ หฅ๚9 ๙๏Sน^kศd "S9 , |๙’า๗&’ึ ๙$ภม†ํๆ&?™'Eฮ๒ษหฬ|š’c๑็ะ1š3iORึพBu"rฌ)ฎ_RVox›]R๎กฒๆ&ลX€๒€@ำ๕…๛Mน๓cฉ๓hIM`Lฅ{Œ|LŒศ,ณ๔๒ฑฟดฯEŽ7U๑ง๛ฉeyนส:T๖าynYœ฿?wi ิ?1‰YถนณS @Lแ๛ฟ5Œ•ฮ1w+ึo+d~ฌ๒T๕;ถœcฆYf๙]dfx9F๙็(\ส_Zช—C๓วบ†๖๙Š1#0e@.rจ๏)ห[ก๕.Yx h&bฎใ[าสPแc^ุ็nีJ›MmsชŸ>๗~œๅ#–ฑ2E๙Kด๎ฌฉTlr^ฎ…o ฎ๘sY€ฉnฎ?ฆ๔}สฯ}์บะ€Ls!8๐๛*t Œ>' ๆบ>v๖T๕หquฤ 3˜ๅฃ“พ๒Xช_ฒjง( z8‹>eล๛J?็OE~SlDฬ๚็๚eJŸงvฤ{J Q๙ฉฟ:๎”บฏ๐ฎ ค6โXฉŸrไ๗RzฅŸš้ฆ‘VF[ f๙ 2€฿Ož‚๊?ึ๊๙ฮsfๆง}h๕ว,._j€ด?f๙‡sฆ“วฮ๖h˜๓)Š?คC0 @f8ื€ดBAap๋หjIีฃa9R@เ™‚Y>ˆฬเ๗‘)FŽง,้‹M๎ €˜5๛ }พ๒ š‡ฃร>หฝ7)๋฿ๆฌ๗่O-แ;V๙k8€8๓]\[r p๎ฉ/ธพโ๚8–๖ฉลoG์๚ Kํรฤฺ:+๘Y™W|x™โฮก๚9eวQแฑM{8 ฅ s่}cหœยQ ›rb›piR @lI 7 ิ0T๎5Œ€๒\ฉU‰ํ0e9ๅ๏-tdxฮs ็ๆRฏฉif™E”™๘ฐ’ ฐC๕ซฤqสขž๊ฟ7 +๔ว2ศQ1เ˜๛#Yตฒ๒๗รคฅuา๖พแฎp}์[๛osŒ€›#เคUฮeฐํ~Ÿ„ว๙SษSฮ;H…Iส]rคVJไธ fV`–.3xz™ชtBš:‡าๆf๑งฌ~ษท๓๏sสœาเ W๑+ศ95ๅ8™โ˜ยคฌiB ค๐฿P๙๛Š?œcยM„b%7ภ๏ r๎้1u‹ตIbAB—มฑห%%P!ต} ณ%ณ เ้ไุI~S}๛ˆัๆฉ™1:_†I7uภcืK}๖๗”UOฅpn.@๊c?9+jˆปR๑ˆนR` อ}ฌ<…๕Ÿ bmJ J”•๊ฏc]3˜%[fเ๗—ฟพ๓๏็P9K๙ยY๛œ/Ÿโ`!ื๚—๊ิสŸณ˜R“ปธธุdธP9†Š“๛€Žoจฐ}‹Ÿ(Œ19 ลไ*ตง’ว€ฉ๗X ฯ™†ก๐ึ™k็ศ์:˜%[f๐xIM่ รbtฟ ย๕๏ว–๓ๅL๊ใ@.(ศQ)ซ?T๖~@p} ำ•;ม+gN7กฮ๗ณปc <n๘ใ+tฮ p๑*8Žอ ะ ณ)3๑-ทฟ‘ฉs ดฤยS €;GAภฑm”ภ+&๒อ2 ฬเฑrŒ๒Oั*rฃ๗ck้™ุ7q.ศ์ว‡}ลอ็๚๘)ๆถไNหœโWภ3แฦ=’ข็V ๘ึพ๏›เ˜[1 €†ฉฎ "aSโนo9๗4ฅ์ร๓’ย?Ž!8ฆญ3˜%[fpœไX’‚๒ป6uB_ŒOอ๎็่๛๐ทค”ฟTง\ฺŸฃ๚%๋?์฿ุ ๅฑญฆ๘ˆŒ@ ๘”ผOัKnฦ๗>dœซ V็‚XŒวบ~9)07ีข็l$<'ฑ €kำ์˜%KๆUว &ยbพ้ลR๘า๒ฝูวR)ฅo"u8vgฟฉห๛Žต ธใ”า)๗มoฤ6ŠJ+bquๆ7ง?”สญหcญz›q|์W?€tŸO้็น/e๙ง”Œ๒็๊8eyวŒpว9ž+วZYaXสฬYzพ ภ?wํ •ฏ๒า๘Œ€?ม/œ ภ}2˜[Epฬ’O๛ฑJ้ษต๚% lฯ)๚XX ค€2u<ฆOfภ,#™ภใ$ื"อษŸปto๊ฤพ\Tk๛s6๖‘–๙q'…Aๆน$41m,ŒณะRภw ๘แ๑W๘ฎฃ—[ึวอ๘O-ไ†ุ$@Ÿ๖?ฅฤ,ูฉ๗Hชณค์c.irgŠ@ก|N้๛J<ฅะน8ฮ0ปžฑฬ.€้"-ใใยคฅlS–๐M3ฟฑ™)ล~(w†ฮ์~i‚฿ิู9qSบ}S, —”‹คhbnฮ5๓€dทwอ] ภ+ิง–•˜๛$…I{9H '>‡IžฎŸs~ง๔ๅ,ฯ@f/9T๘›ขปcห๗RŠ=ๅ฿7™แฉฝs'๘q@šเ›ีŸณพ?็นฝ็ไ๚h๑)ฤงภรM„j!˜Iิธิ'qณ8 -\ล•ใฆ‰๙฿c๗#็XrกR b;ƒ€g*ณ OrOL๙s ถl/gวฝ˜’O€\`!ั๑๕็L๐“(กHอ๊ฯก๚c>€i@บฑpโŸzr`˜ฮ?-9ฉ->d~%0›$ศน |& 5๓C€Xด1…ฆŸฆP’{F K}€)ผฏ’[ภMศ ŸฉXŸ…`7e๕ฯฌภg&3HKJ๙ฤฟ4ณ?T)+?๕cŽก๙ตP_7g9Ÿ@๘}ฬ$?.žฒ๒็ชw>4ด์น๋ฤA@วฑœ‡ภK@ภwh*pc!iยŸtœCE?Lอ3ล๏ํ+ฬchc•ฟดษR.[เƒ$JmJ๕ืภ”๘Y>r™€,1๋?W๙็ะS•ฝ๛+ Lฅ๙Sพ}pึพไ๏็๚/์๏ฉJŸ‹“ฌx์"pœกฯืๅฆแ๐–,CแภE~รr•WN8ุse„@ภ)?\ฺQPฒBSึ?ภ‡น้%ฟTNฬ๕ย€๚.ฆ›HXช\ู้แ\+ภ=ฟ0ห3๐’ฃp่เุWถ1‹ฟ^มง@jmxœšอ๏ะHJ_๊_ูsy ]ฃ$ู๒P!ขP€ˆHไภต%`7T€%ฒDขๅ/‰ไ๛ON๙ycnะ=rhˆ[œ ฮYฯแ1w?8ๅœ#9้csค:คๆaค,‚ธาๆŽ%ภMพT1 @^1 คsX-ษ-0‡OPŽ™-นKฎ’’,}*zIนว€@ pส^ฒฅบ๋ใฑ#R†วน๗ง ่JGDDlฯP(จ‚าˆส*จ5‚ึ]Z5`จต๚ษXK`ขฆ!h P๛g[p@ˆˆฺ๔=hhวม08ษ๕Qง,kษ.)้e@\มล|ฮœี?๕8%tD\hๅฦ~s็ค๚2ฆ๘9ๅฯ)TZฎิ}‹อี€\ฮJŠY>™ภPRV~˜Fš่'Yู‡v$…^&‘^ว๘๗sf๔งf๕ใ็‡HX์^๕Š6ฆPX” หJฉขB,+…E…XT ‹UQ!ข)ตAิฐ „่)จฉ ššhฟ'ชwD๛ู}๛K๛ต๛ญฅถ;ิ{KMb‚ฎŽว(วcULa็L2“ฮsฎ AbmŒ…IrฌยI)XN™(}I1ถ?Cฮmศ”rsอฐฎน๗1ึ_\_?๖žฮ๒ศswLก๚]˜๔—ณ›Ÿdษ๛ฟา_ฎ๒O๙๙s—๑ฅจ~ษา‡ฤoฎีR‘\z4…ยฒRธXi\žhฝ>5juข๕๊Tซๅ‰Rห•ยjฅTตDUV   ข๊v๓ˆ€ศ4 @]ี;ฒ๛-ัvKvท!ฺงำY@y๎@’0E๙‡๛H4ฮ_ ฤ&๚IVฬฟ?…๊็ŽรพŒ+ˆฤuิ>ขาJuผiญ๛jฉpนึjuชีษ™ั'็Fฏฯด^Ÿjต:ีzฑFตX)U-หชข๔Xี–‡่ฒ@ึ€=ดV–hป!ป} ปนณ๖แฎi๎n{๗พn๎ื๖๎ฆ›ปฦn,ํทึ๎w–๊šจฉ-5ฮu`ษM,Œ))ฬ๏ฃ”Eวyสm*‹˜ีึ›;ๆไPฑถไธW$ห[ข๋s@‘†๛DจS“}@ภKิ…e8‰MˆAภ' ฯไPะ)K6ฆฅน ฟ„|ภอ๔S๛Sท๎!L๊ำษฎ'‚VaW …‹•VหฃืงZญฯŒ>9ืj}ฎ๕ษนึ๋SฅV'Z/ืJ-VจชๅA้ทŠ:๚ฟ *l็ ธช’m:W@ิ์‘๊จvทฑv๛`้แพฑทp‚›ฺ๖ภ n๎oj๛p฿ุํƒตป…“๘> •พŽBพ”u)ฅ—Cs vx๋Rทvสcฮ7ล%‘๋R‰ญˆ˜๊R;3ฦ€@ธหc ฤV 8ษ]-0ƒ€OXž+˜โžj๙KJŸ›ไ')๐Xšแ/gปุคพฉ;๘…}•๊ืT\›”F4…ยjฅq}f๔ูฅั็ื…พxQ/Œ9ป2๚์R™๕™RหuO๕cQCจM[FK๗#(ˆ A) ;ๅชฅ@€ !!a ๚๙@๕žhฟณดXฺ<4๖แถฑท๏๋ๆๆํพy๗พ~๛หฎ~๓หฎy๛หฎ~๗พนyทo๎ุm-ูฆŸ/่D\c}*wย0ง O rส€ 8 ไฆ‘าMQaฆฬ๐c๕r€ษๆ;ภnล‡m฿ไปvpแ๗S˜ฐasnผY>"yŽ ฅtb3ง๘9 ัฅ๗+Žภ‡ุน/‡๖็๚+ี๗lฤžโS8Š_ซลZซ๕ฉVงWฦœ_๚ฺ˜‹…9ฟ2ๆ๔R้๕™าหT•ฃ๘  6ไae€ง๐Uฏ๘ฝ?P=@Tะ9 ›n(Kิญจw–v[k7–๎šๆ๖พนyณ7๏~ีoู5o~ูีo5oี7o๗อMl๎ป4ด฿Y[๏‰,๏เ๚Oข`ม@Vh)๗dฤƒ—@VฦSฌภวฆ›2!ฆ๐ฆ€c–…J@หา@ t38V€› ‚0nžว คๆ8‰อ_™ๅ#’็ธ 3ฯQ๘L˜ล๏+๚ุql2 gว–rŠล‘๐T_งยA)ฤขTธXkurfิษ…ัgWฦœ]บต๔ต9ฝะ๚ไ\้ีฉRหตา‹จj ช(Mh4@7W ] 8ฐ๒J^กTฺ ำ‡p<่ใ=ะญ h‚ฆถ#ะุํฆกอ}c๎j{wณon฿ํ›wovอ_ทอ›_ถ๕›Ÿท๕›Ÿ[P๐ํฎนปฉ›ฦฺ ซbt6–R์0! „kไ ๐Sฌรด1ื@.รcs#R $E^Gฮ๋ ycซr๗|ศuใ™AภG$ฯ‘๐eŠ‚สz p,@.v๗ใ๊"-็“@ุV€8H๕eดOฑ E…`บI}หตQง—F_พ,อๅซย\ฝ.ŠหW…9ฟ6ๆไB้๕ ชล TQ26€Znอฟณ๔กmuีDบW๔ u๛ ]8๊Œฐx๘RFPAhdIฦBำXช๗ ํv m๎k{๗~o฿ถmผฉืfŸ>์ืร—๋๚ทŸทx๗พnv[k๋†ศZB ่@™ฒผ8๗€<็ฦ…’'วสฑ๓r@JุžX{s&JJ“%:^ข๛C@3้รc?ญ๎ฟ๓ว ธq,dภkW๊^ฬLภ'*ฯ„’šํŸ;้/ถsŸค๔9ๅ_2๙คOsJalวŽ๒?๗›๊ืQ˜าnํ>*7“Y็W…นxiฬล cฮ_svีY'จซ%ชฒ$T†@),ใRN๋ฮช๗?j@u8€นˆpป‹นบ„@Dhญ…ฆi`ฟk่๔ฌถ'็;}vฑิg—}qฝะWฏ–ๆ๚ีร็?์฿ฒmฟูืท7{ปนo์~ks—๚}œr๘aนŒ๗JฬŸ*™'0คHเ&5"ตูทป"ท๗j@ธไ/TPVœr' ๚€€[! ๕Iุวแ•˜ฬ เ#‘็ไˆMKอh๗-eฮาๆฟ4ษฯญผ฿” €›[฿ึ;ถW?ืfˆฤ็๔iT”rึพV'็…>ปj}๚/‹^แŸ^jฝ>Cฝ:AตXก* ส’ฺ™}ๅ฿ั๓พๅฎ<ฅ>T๘ฦ*P!ภ!่ฏำ"๗aพ“เ,X"ฐM๛}CปmC›‡ฝฝฟูป๗;๛๎อถ๙ํ?๕/พ฿็Ÿ๛ใ~Ÿyุฟ๙y[฿ผ7›๛&ˆงว8M?†โŸ"OQn „คม€.q~๒ุN9;Jพฺ‹“~รฐ˜+!ตl1ๆศu ฤ๚˜๛>ห”็ยLฑRc,€ดnžแฒฟิ๒ฟ)“๒น ีฯ๙๛cmๆw>:…้&๘U ฅkญN/ŒพxYšซื-ี๕EQ\ผ0ๆไ\้ๅ‰๓ํjM 5กRจZ‹ปUถO๒ ่ล?ฒ๛_5 8œ่O tระบ~ t4 ( M ลฒฒธ\7xvQ๋‹šฎ๏v๖ล๋ฅy๑za._›๕Yก+ฃช…VEนฟ{ณkv›†š†ˆlrี@่Ž‘(Y?ิ99r์ ~,ํฯล็€X?ค@$ เ[๒า>Žย†1เา†l€ณ๚}w€ W pฟ’k€s p“รg0‡๚Ÿ™€?Xž เ$ๆ็๗s'N๙สวLู(ฆ@๊—|๘วฬ๎๖)ช–๊/J-VZญNด:นะ๚๔ย่๓kฃฯฏ[šJ›ำ+mNฮQ/Vคส`Q*M ฐUจˆํ8s ๚;K๗ํ+_ม›ƒฅฒ๒วไB `ปภpฑ่Žp๐$hŽ™DญตP-jฌ%.ช—ห—๋ W'•:ฟZ่ซ— ฏพำ?O๋plต.Ÿฮผฑ๛ยนคฒr”SSภิz ยs H.„.n—ฟpฎBWŸถf~%ท€๏ภเX๊S_๑วาๅ*๗๒น€œI~ต-Y9“B—ภิM€ค™าVฟS๚Sญฃg๙+Q+ญN. }ั—/น|iฬ๙ต6'็JญฮP/ืจชจขjบ {Z‹_ม@ี"h๚ั$>ฺ?(z)๛/แะาu"จ~๊ๆาX["บ่u„๊S  ถ PƒBƒZUV.ื•:ฟ\˜_,อห/—ๆ๊eต๛วีญ็พ๋ฟ๏๗ถูึป%kฃ๓%%Pฒย8 ๎ฑ–๙ษ-๋Xp๒!ทปขBื@hีsฌ€Kcผsย`ย29K฿w „ใ‚ย๙n>Aln@ุ็า<A๒นN0r<๕S๚_l๏้ภ’๒M๚ใ&IŠ?eแOถ๖[ƒผ๛๒žn๗่ฏ’พsฃฯ_-ีช(._s๑Bงีฟ4-ีจ,(ๅ,~่่~็wวrW Xฅ?เ+Ž-€แบ~€ก8จฺถ4ข|=v-ต\@‡๚ๅ๎สํnD ตฦฒ4ธXxrV่ำ‹RฏO ตXตXUUทBโๆmห์wถลฤV$็q๗’”>]‹ฯ/‘๋๐wๅ‹น8Vภw}_=G฿s ^'ŽC เ3qH๙;ๅ‚_b+bLภ์๘Hไน€ๅฆใ,œ-cŸ๘•.žs!ค>๊#11_?ื\Eาท ม”J-–Z-OฺํyOฯ>ฝา๚์ส่๓+mฮฎŒ9ฝิ๚ไ๕๚ Zชฟ"ิu๛lOกf๖ป™๗z@๙#k้๋azpTฟOๅญก๖?r ผKGN‰!zฟ-HAภ^ัw,9ภโ)๏Zช›XุnVิฮะZ)…ช0FใbYเr]จ๕‰ัgล๎_๗n๓ฟ7๕อป=์w‡/3๗ƒณ่ฅ๗!f็”๑ƒvn}๔)ืGN|x<ธ0g๕๛ส฿‹อเจะ-เ3!ฝZ!ว(ๆ~น๙Aพ[ ๑๎‹ิ—|๎>ฬ เ#”็R Oฒ’sh-€SŸ๔ๅฟ)ๅฃ๖งฌฦ์xm‹…ยๅ‰Q'็ฦ\พ,ฬๅซ–๊ฟ8P2>Pๅะ!PPฉร˜๊–ู–นๅw*ฐ๐ีุ๚๏๙‹Ÿk>ัแZเvเ! 0p„>v้œ‚‡ด๎ร/๚็่—๖Wบญ ”%โ้นREiฬjUจ๕ฉังgF]ชฌZฐ–เฆnv; 4vSŠ”ƒ;นฎI~€pLŽS[ฆใ๖ิ๗ม€ัŽpq>.๔ฯŒญษฺืBxhืf   a[ฆ๔๙ `yNภษ@lfXJ๙็ิWb{งฌ!#5Z#๊ปu—งZŸ\uveฺ |^™nฏ~mN/P/ืจห`Qด๛P๙๛แคฃ์=ฺ฿W๚ฝ…t ขฯฟ๋šฮ?(๎”Z[ง๘Ž=(ัVฅ๗พz<4๑เหืpXž7ค๗1์’NyxJบ}๗โoc”tš•)ง›่’uS 4ม ppeโ%„7B›‘@k„ชา็ฅBEF™vnEตะธ>+ิqƒ๗@ฟlทึRำ—9ด๗1ส๓)ๆL‰ๅx\ูR|H๛„Š> ฯ™,่๒r าœษทฌฯH[) 3(bnภT้~ฮ เwฯLU~˜๔—รฤ–Jึผ™9H๙๛ฅ%๐๋Ÿคฏธ5ศRธ<ั๚๔ฒŸุW\พ6ๆโ…)ฮฏ[~kํ%tJ฿‚า„ชSภะ[ู-U๎ฆๅ)่'๚y ™.๏>r8h(ฝา๎/[#MศำเๅS@๔ัช„!หะข W๘ื€ทล0@ฌrกีฉฎำ‚ีRฉjกม~g‰,ผรๆกe™๊cgŸ‰้SV๘ฑ๕ษerโc <๗•o๙๐ŠŸc ฒ8@เƒ฿5เ๎k€าj้Ošหบ$#ษa_‡.ฎsฉ|`๙'’์ย$ฅ*ึห?ต@ฺ' ๖ฒฆ(isŸวR๎Sผชจห…ยลJฉีig๕_isโฐœฯญแ_ฎQ 1ะRHะฏw๎}`วQ๗กโ?X๛ผฅคฺp4ฤ7‹ฟ=Eอ๏ƒฦัเ”?ฐฏ›7yเฅJญั(ฤส "$"ฺmฒ5)๔{x๛+ม๖ก!ฮ_”๎qlย —6๚|$$วRๅrส่1m”งุc€ภ2y•ซ~+ น8๊?5™cb,ขคx—G(9๎ชY~y ฅ๘$ลฃฅ—๊ B้าdฟๅ(*W)ฤrกิ๒ฤ่๕Y;“์ฺ˜๓+ญฯผM{ึgJ/ื ชฉjAhJ@ญ๓(„ƒเ`้‡ฟ๛‡>ํญŒ\W#>๔ใUz0ดะžƒ๐,๗No`ฟูK๋w .•ฺ๒Žป:๘.ƒ๎ 90 ท@ฐห`Wธึ€‹•V็T‚mVฅตฆlAAฝทd-ูŽภพ!ฃฬ9…ษฅอ}n่ศ|น’๋ฦHต—„c—†2ฯนc๘ ม08V^z„s8› Wa| p.ฎง‚€gไsœ,…ฅ\!ศQ๘9Š>fๅวๅc@:ดฒWk@m– ฤี™ัง—ฦœ_s๙ฺM์ำๆ๔J๋“s4‹5จฒ4E๋WŠ=ƒœ(ดpแ`๕c[~o}ฏy!ภ*ฦภš๏๏&ใต ภ›%‡ิ.ใo๒@gœŒCผณิ็€›/เนBื€s‹x[ ธ\kP**M €`๓ฐท›Mm‰ น}_7‡&ณ ฅ„ํ"๐–7โ่yฯึก‰‡ธ๐๙หo้c ู<ๆฆvu˜†d๑j%วUแ๚&FQ‡ส_Ž €ฯ p+฿pโ ๛ }น1V‘’L™8S |n วฺwฟน Ÿcrญ•&อ%ˆ)„๋Q็hƒhสึฟ\ญ”Z(ต:k?ศszeฬูต6็Wฺœ_iฝ>WzuŠzฑUv>vป^ฏL‘ๆwV๛มโwU๎s!N‚a๗0C฿c[0b|฿  Fกp<"แธอ๘(ซj่2 ภM€–€z7cI"CธXก:%์ถห๒no›†@kม?6ะิิ์wvสš๓œD'ีAทŸAห้n๓"ฃŒFิAซ๎W#* ถ ‘jงEวC_~‰,Y `-Qcš†จฑิ6uCTืDuwX"kษW1—wœฃŒ$ๅ๏วI๓bฎ๎/œ,่ยย1สgRcUฬจเค๑ลkฃ๔\ล๚xP>'๛0ฦจอ˜+@b ?ฉ|ฮ‘Bๆา๑จ?”A,WgZŸ^h}vีํอBทK๘.•^Ÿฃ^ž ^,ฑฃ฿*SดิดRz:<š ๐แ7ƒ& จฮlWCš฿Yมฝม,|Žโ.` T๔ิอ๗;์`=Žๆ xJฟาAศป:๕K‡เเภ&๖ึฐsะก๏ไDT„ีเ์ย่ืUQืkา‘`ทฑด}hˆ,ูฦRธO@Lr”Ž`4‚1 A,ŒยฒTฐ(•ZT ซฒ๛+–%bQ((Zp€ฦ hจ5v_vtOหกท‰,ุจ๎”พ&ุ๏‰v5ัngํvgiปณดZzุ6ดูZป[ฺ๏-ํ๋4 DŽศaLB€ห„ƒมฑข๘ ™฿pl@ส๒O1Œิ)๗™ K'นf๐ไsนž,=ุ9Œ@j"Mสาฯ}!ง(~ •๛บ" ‚vfyX.ฎNuK๕ฟh7๎นtTฅ2ซ3Tห5่ข‚ซ|จBkญ+ผงๆ =T๚ฏฦ hZฏแeรI๕ แถฬ๋“ขึ+w๊ยƒQ‹?ศa>hลI, ;แชต๑ฐ๋ ทถ๎€ล ิลต1–* ฐธyุ๗o๖v๓P ภๆา&`๐&.vV:ถV;bซฐ•j]Eฦ FaY(ฌJฤฒิX• •ยีBซๅBแrกqูฒlA@Y(, ba:—S๖Žtฝึ ะX ฆ!ุืD๛ฺยnOvท#ฺดŠ฿>l-=l{ะะฦฺอฆiใv–v;KปัพถT7Dึต์@{k,ต^๊ถY:nชห T๊พ<œ0ไภpcHธRภCยyF1๋_|Fุ‡[ฑ~ฟฮLภ๏(Ÿ เ„SˆศU dภ)œYท9n†”ตฯต™{YP๋vโXตRjyข๔๚Lซ๕น2ง—บ[ฟฏ๕้ฅ6'JฏฯP/O@WKภขฅ5tVšปะP) –๓3ฺ%ๅ.•~ ๅ<˜`wฐธึ~G๔๔zวฉ๑el๑˜‚žs่7"ภ`ทมž@ฅ+RWส4ต๛›…ฝ{ฟ'k- ‚/ี๛qƒฆ๋+ญฑS๎ํ฿ฒRธX(ตฌ4ถส]ฉๅBใช๛].”ZTบOW• %bUถ กpDฃZื@ (:w€รmฎปˆภัๅ_ํขžhืต,ูอึาfc้acํรฆ๗ ?X{ฟiู‡M 6›ฦ>l-mท๎,ํjยบฆ๐ฦ๙ส<%โปC%ง` B7ฏ่รฐpžภ ?นFฮ”’ิ๗f๐;ส็RT2iRภเ˜ 5นฟ.8’๓าฑL€j-~U-ฎฮŒ~m.^ถ–ูUซ๔Wงจ+๗U>r_ๅญ๐@Mwลvธ๋•ธw๎M๚){ฮวะ=เฑcหะฒม,}t–u7นป๙@งnƒฑรอศ‡๑H๏Ew*qNแ~๓œ_ย)๚ฬw–fทโ zc6ู;ngBm—'คฮฅฟ๘ฎ(ถ›…ติZw7>Xjjข)C'" ึ สaQjต^iS๚๔ส่–๊ืๆ…)ฮ_hszฎฬ๒ีb‰ช(AiCpXฦฝี‰อZ๐เ{วพฯ^๚๋-\๐^๑C˜๓๓;ๅI=๛เ€๕J๑ ้ฐฟ๏`>tL@\๙ำ! ~ธณ“ŸศปึaJzป๊ ห็”ฟ+@@œXH‡k๔ํv๓BQฎN@_พTดด฿•t๓vg฿jšถกอต๕žศ/กอซฐ}Fœ?พ๓฿; ื+ญฮึF}~Z่‹3ฃฮO>;ั๊์$PKฅ–•R‹RaQv๔พึว๏ฌ|ี~๗AแM๊ปธk˜? ะ‹๔ YKะM joPื@ัรฦฺ๛ฅป๛ฦทเ๖ฮ6๏๏๛ฆnพฏํป›}๓๖} nn{๛P‡‡ฦnvึ๎v–๊š`฿M.l"k‰œป†O-@>•>๒ย%„!เณnแ>๗›3IใหT‹?๔ƒ๙’ 8…?ƒ€'’็8‘jŽฒฯe\:i-๗๚๕Ks็}ธึˆีJฉี™าง—ZŸฟะE?ฑ๏J๋“sฅืgี฿~ GตA๑–์๙ฑ~J๗ฐ^=Tp@มคถP้ฟฐื):g้ƒฏก_ึื+์า๗J๙0ฑw€—8ใpX…ํ%๊๛ส›ax๏ุ~Y”๔.‹{uฬ˜ยโ๚”๔‹/์ถ†V4o-šฎก_m ๕พ๑ฬ@ ฑฅๆ—KญNVOืZž}vjิ๙‰QงFuvาZ'+ญNึZญ—Jต}ฤEฅิขDฌ*…eXˆฆง๗;%ฏฯ @ๅแ้๕่œัำฮ5พ{จฝ•Dะn7eจ/uดฏQํ๗šถ;M]๋&ุlจ/๐`ํƒตทwตฝฝkš๗wฝนm์ป›ฺพปฉ›Žh0ธนซํ}m7ŽU๑ฯฬc๘Š? wqG‡RcVh๙ป๐”๕ฯI ฤqJIฬด3๘@๒œ€๔cฦŸไงวศ/‚Œฒsะ๗1m; ‚)ื็Z_พึล๕—ฆx๑UQ^~ก‹ำห๖ซ|‹ชฒBe mๆ=†๕ชB๎ใ6บttส์ํa๖าzดฟ›ิึo†CAS\v็ G}ภภh;^๐ฟ๎‡&ภ[ืƒ๐zฬำฑƒQลSc_Q*~๒ุƒกr๏ห๊Hƒqศ๋XjI๊ v[,}kšทฟอ๖กกํั๖มา~ื๖ i้}\TญBฟ8+ิีEกฏ/ โชิ/. }}Q่ซ๓B_œถTzู๚๘ชฒ@, €ั€FฃS๔จ:…฿1GˆHฮง€œบ0ฟฯนc๎‘w A๙@ตWาT€!ถL&kšฆe ๊`_ํI„๖acฺ้›ปฦพ}฿4ฟพซ›฿์›_์๋_฿์š_~ีฟผู5ฟฝ›ปฺ?4vืบ :+z,#>Ž;เฟlุโCรฤIcW–4. ˆl’„‚B‡ึฯ™h#ีซ(Pภjฅี๙ c^~S–/พ6ๅ๕—ฆธxกŠี๊j X– ฺA}˜ฆHฎtๅ๎[ํะ+qp–ฟ?A \<Œ๓Vฟ๏็๏ณ ฏ}›๚|รuา>G๋cgA๛ VธK{+ †๎w๒@I;๖สฟฏ๛P๙ท;พ๒n๋ว€ŽC7ยipมkฟ{ L€H๊ไิีke^cŠ๛›ยฟทvOToกฅ๖WZ,:=1๊โฬ๔ส๚ขะW—ญโฟ<3๚ิ่ำึโวEฅTY*,Z บsนฯ8cงุฟ‡Ga0ขDม“,†ง^โรƒำ“p๕2j—Tฺ i€๊บDธู?X{sgํป›บy๛ฎnผซอoo๗อฏ-h~}ณkพ฿7๏nj{sื4๗5=lญn[@ฐ๏Lํc@ |ฏCEฏXp€pฐข}ฦภ;87@86๙๙!’6&†ปsw]ฟ\|@๙ิfฤq/wž‹‚ฅฅ3) )yŽน:ลา๖'J#˜Jแ๚L๋ซ/สโ๕๗‹๊ลWฆ8ปDณ#์ภภภŠฦ๏‘;€ผcคํW7x“ กŸ๏0 %@นฐ๊์ ๔ซฏUฑน5๖ต๔ H5H—็…พพ,๕‹‹B__•ๆ๚ขPgF_œ}zขี้Jซ๕JฉีBแฒjg๑—e;?ภ@ง๘ปo'"1Oชฏ๐CๅŸi๑Oฑ+๛๒ €ฟน]ดPถ]kJ jดZก:=U๖๊B๋‡—…}x บปošึ5ะุ7๏๋ๆทท๛ๆ—฿vอ/o๖u๛ปkผkมอ]cญmˆh จsนญ มAจ๘9๗€ ฮ!ศ Lžุ]ศQธcม1ใ%‚’๛‚ใบฬ"ศงฆHL๙†๑9 <ๆำY๔ \Ÿซ#dไๆog4ฆ๕Ÿ^ๆ๊UY\ผ0ล๒tYu;๗)‚pซิVWKŠSมF<%๏ฌ๐ฐ,่๓๕Y:ๅ/฿เว๏ๅฎwสw€Wพืbd ๚มทาปุมค@Œ่|๐ฌ๚^กaZ๘  ˜p็A€฿a…ฑpz ๊ีkcš;ฐ๛7h—–  ฏฎ+๓๚Ei^ฟ(อซ๋R_๚์T๋“e;{ฟ๊–้™ฮสื @iD์'„า-(~โ•?zc?๗$็ผyมƒ>dเแP$v๓4 &ภขRดด mำm<ิ ์๗D[วZ๛ๆพ๙๙ท}ำ/ป๚฿ูึ๓ŸM๏Ÿท๕~ึฟพู5๏okปYปฏU ํ#8rค”ฟฏธ9ทiธย^–ŒŒ8ˆ—a{BeA0yคk็‚€™x„|n`ช !)้\6เ1๒ห—ำF)ฯ @c4eeeิแำผl[H?!ฎณ{ๅ๎4/๖สผต –a~@›gฐ‰ ม` F๐€Œ์วฒA‚;”`oก;ส{ธด๋4ฌy8T``มvฎŽยwn่”๗5DขC:์าตส+ซO๗ํ4๕W€Pi@\*U\*]}iŠีัอ ๊ 5\_”ๆ๚ขะW…พ:?๘๗ซฒ›ภื)รFOฅ๏แพƒO7upLAsvพtSค@L๗ยชวนjนuฯ>ตˆ’„}Mpบ{vf้\๋ห ฃ_\ๆ‹—eีU๓ฏๆง_v๕ฯฟ๎๊_฿๎š7๏๖๖m5๖~ำุํึฺบฑ)F ฌฑ‚ฑข [โง๑AAุi‡๗ไŽK)แ~GBX˜ื฿)0ไlf%ไsำ,ๅรoญ{ˆค“^จX}รฒีVj๗J‡ฆhjคฆVdkCdบ๘V๙4ฝeyศ์iํ~Vh1{“}๋฿มฮ•ีฯJ๏k?ธๆะ]บ| ะ^สSฝ฿ภ@oaใ@๙Žป๔ฎ.8Pไตืณฦ=R่˜•ƒ+ „x8์โih้cFaพะฝ0`"ผ”4 (ิธ(”:]ธ|ญ๑ ญT…ถj\/:Yบ๚VUซ๘‹v`้็˜ฺ๏ภ€oอณส฿:ๅ?…฿BศOŒ~ธว>yG w๔รax์Xj'2@iPฆ@ชOO”บพ*๔ื๗ฅนน[ุw๏›ๆื7๛ๆ็_w๕O?o๋g[๋?๚฿?o๋Ÿึo฿๏แaCิุั’ฮสๅฦH{G‚?)œ๛RBย9 q’+ภWาxษ)๑ฮอ’/Ÿ ศyxcJ9็Œ4)$=ลาOต“#ง ๊ั๖ž์ๆ์๖mYiด”R„ํj @๋)4๐†_yร`i^kyชา{ ฌฟณไๆW๚<๏xˆร๕๏—ๅ>Œ”กCZŸ"สฟ-๑ #<‹฿งฐ{ซ๛  _๙ทซํะN‰cฏ๘ํภ๊ว4 า๚ึฟk๙ahPญโ7€P‚!…*ภ, …h*ฃิฮ@ Jฃฑ4ˆฺjีฯเ๗ึๅ(ะโo;ŒSLุˆ๒OXI ฅ™xฯ ‚0žย“ํ=xZ!hƒXVˆดB8ท ๊บ€ฎuผ฿4ฟฝู7?ฒซ_kณฟพ์/ฮ uบ6๊ง_ถ๕ป›}ว4ํ~ํถรพk€œ๒‡เmณศc,€tWค—ฌ๚Tฮฑศ\KR๒3;๐ัt)ฑก$ฆฌงnล+}นฯxฟ†9?๖ำภน๋ ฯ(‡ขิธ\ฝX]T ห…QๅB+c๚๏ฎ๕™ฺqา๚รโ‡“ปช‡ฏธ๖€~I ‚W6yมํx8๎หp›y๕้๋๊ีgฐใ`?ุwuE๊}่ญ-’ยh รp|๒–ฒ๙ใ—wŽ`‡๑ุํฯ‚‡ฝZZๅ๏Žฝ=\˜ด€n$M ”ค "K2ฐ„–PยJ\a‰+Uเา\—W•Vหชณ฿ €[ฏ฿าไึํ๗ตp๘ๅยPฤ œมŸ0R”๔นื ร8K?%น†Qฺš%[žH}h'ต‰O๘‘cๅ.)z<ˆH)%„~[lป)”2bQjต\ฝXส‡11Pภเซ|_ฑ๚้ภK7H5T๘_Mเ่&ƒO {ต;˜ฆ๕>ธฎ๗ ๓ xO zใ๚–ฝO๓ปs+~์ว8_๙;E‚ยo`ฌ›Cท้†›ผY@ P@` „)X‚ฐ† VPมŠ*X@P‚ฤ~อพvŸํํ6ๅ้g๕๗ใ5-Œ‚W]จ \‘'VŒ)?œ_†็$ชq›฿{ ผ?ีMผ-+ฤ๕Rซ๓ณvžล‹หRฟผชฬ๕eฉฯN ]UZHPืu>ภพหรง5ฉฤ%e‰Ršc•*็็า„uIสเ๚p–„|๎ภR๑ฤv๘‹}ฮ7ืฒ?ึ๚ฯ]V8zyQ!*…ˆชหuกซUกŒiร฿`๗ฉg์y่ใ็ก%ฆ … :Œ๗ „+!ๆ}mP:Ÿฆ>ะ๊ƒษ๋žท๚@ภmถ็€@ฒ๏?๎ฦZ;,`o๑h(ก ะ๒7ฮ๊๗4P Qต๔ตG๕๗”ฟ๛๋ucญ๗ึพฯY1„แ‘'Vzk .D “€@$ ผ๒ผบ"ถ{ihำ~8iQ)ตZjuฒ6๚ดS'kฃVKญG• ฃPkํ๗ฺื“Dฃ wย๐P$!)๖,ใ$วข—\)c •Ÿำณ$ไน้/‡-vLฅ๘ฅ/jเTข€เถI%ะFaต์\…Bm4jฃPฉa1พต๏ภš๖: ,€๎[ฦ{€ภS๎„e ~8iฝ3_๙๗ส…ส?ช๘mŽƒsp๔พณ๔มฃ๛๛ ›ญล_ยZชีY+,`‰, „ J( ฆU๚ ฺ๙ž1;Pแ{xฺศ{โ(x๊"?xแภค๗ŸR็WOAุ๒๖รผ_ภq๘ภj๗ห๑:ภ…sJปFย”PNpั)ญ‹@ต๛&ด @- WKญฮN }yV่หณR_œ}ฒ6ช4mฅฌ%จ‚ๆ๐gฬ๕ำ็(|ˆ”ใ‡qบ$1ชŸ„_ ฤสแฺ aณ๒ภศึด๒—€žRภี+ฌฏิN€N55ตE[ B,*ฆิh ฅŠJcQhฅ‚๚ทงอ{ฅ๏+๛แƒEˆE/ œฅ๋แƒๅ~€>8˜ O›วค?ใน^ปฒW‡‰z#‹๏x…ฯ*๙ึฺo‹ทู|Š B( aIVคa ึPยKXa–Pb ะจAcซ๘ฑw#&๙๕8Kไ–"ส~ค๘}ถ„‹‡q|ŽLุแ?ักR๗ำrฟกร9 ‚ฐ@๙;pภ‡@็(…จ BU*\ฏŒบ8+๔ีya.ฯK}yV๊“•ัฆPmOR;' n?8>ฝ’2—ภDาs ˜2นฒbณx…ฮ)๗&า p,ต}–@>e€‰ใฉ€-Ÿซไง้zศœ‹6ำ(ผc‘›†@!ข6 ตAิ…ฦฒาช(4*ฅฐทถC๚ 8แฮ ?จd^ ”}|@แ+๚>?zeyŠŸ"ใxมร1ใษ~ฝ5๏Sๅ๎‡ ฿๓๏Kึ=๕วmV๙[ฐึ5d‰ภ* `:ลฟ€ฮฯฮ๊/a‰,ฐ€RP`g๕w6ฟ๊๏gจ๓่ ใXX&ฝ?ฒ๎%F q์8`$๑>‡| ำธใ’๖Dท€$:FU๛ง5baซJซๅขฆยjกีrฉีฒาX– ซฒ5ัดn;@ฐn1  ฦ Fสษ‹3:ŽฅŽqได–@>U{XSสQ๚ ฦc]a\Š)F์OjืฮCฟดX‰lC v)4ZกึดVxุเวSื>(๐๐hฆพห3 ๅณ๒)Œ„ล”cฮ$ภ฿คX€R๚C; [+ขู[j๖(5šBฃ้ๆhญa พฒ@@TI7 -ั๓~รษ‚ฌ_‚๘`Rโแ piฝวใศ!l่ื๏-~–ึ'g๏ำธฟpG๓ป?Knฟj๖vOd๗ฐG*KPฐF'h๐DpขŠฮ๊7Pจ e@ซึืจ[ปp‡Xล‘ฦY๔าฑ6|…cาโ๐8ล๋ฅแุV)3>}ษ๒๗9…/‚&ฯ›0Xถ๓ฮO>?+๕ูIกV Uกq฿บ ฑฅ ‘bฯฦ~sาpwM’ฉKŽ๑๙‡๎€0M7ƒ€LyNภWาqL๑วVpพ(ศqH`%๖ม1%lj BTฆ]ะฎeึJw็ˆJ ‹R๔#ยฐฃH}%8๚ใัoุ๐h<ภ`ฆz|ฌ๏วƒๅ๏”?}ยะW๐่|๘ง‡ณ๛ั๓๗ดTSํvDข=ะ,mtTY+ฅแT<1ฎu+m`ก ”hZ‹_iPh@Ag๕{ส๔VL?‚ใุคฟ๐M!LR๔ƒxdโฅ๊”ฟ฿ ๕ฐa1ซŸ9Y๙”a`[ _5PVZU•ยชh?พดXhUvซสRกQุo๐e;โ(่อ\Pซs(B|ฆ์˜cˆ”aณ!ฯpŠTbbส<5Apjพ=R ‚ใพ_:Kˆlmษ6DZ#–•ฦขPจตBฅฝ•ไญ๗๋M7ภมมP1๛0 € 8@ x81L(@ก tษเษqณ๗ฦ›๚ดณ๎๙Yอ๏ฑเ๛๘ญล_ืข๛ฒ7dSณฟ‹EKาpฆ œ›ฯหฯ ฃVฦ@ฉ Jƒ๎ฐง๛U พpึ<๗–$?เใ7ฺแ ็žH้†ฐi}+ผv๙J†วE๏[๎1€พbั‰‚ฬ$ภA}ร๔~ฝ,b;Aณ0ˆซEป|๐์คPgง…>Yฅ5B]ํk‚บถดฏ๚S์ฑใ0l ˆxŒ2ๆฯไธธp ธ`$๎Yห็ฒ๐1’๛@ฤาI/ฤc,ค‘sXงXqะ์‰๎฿๏-YชmP  ƒZทฃำ Mห?ขn• uƒตฌ–๖๐1›ƒ)เ›‡ฃUโ‡=๚ช}ภ…นดํ๖พ~พn”๔P‚Vแถ๗ลมVฟแก>Œ๚x๒iG๓“ํ๖โทิ๔t?‘็ฯ'ข_7ำาญ‡รพ-ีO–จiฺo3์wd7๗D›;hš ๎JRdJ…Kฃีxฎ œjKc ะ ดj•ฝ๎~o๑๛๓ยวQ 9_พไณg 3)>๕6 €BโqE.ฃoM{็ƒc`p_#fC?ฅ‘4|\YDใทฺe—ืJใขRjฝา๊์ค]1pqZจชTจมDc{ปkถตwธ[มpœ๐•ปบw์z7F"7›฿BซCRใ‡Šƒ™ดฑ3Œธฑฯา!ืพ™˜(ฯ•)๒r,๘๋_'สDเ˜ีฯฺ^๊2hฺ็ค1ฅฃ €mœwIP๚\X์XŒ๗ญๅ0ญคคฐภฺฮV”lแฌ# ธ๚|ACf€k‡ว\ @ 5BaฺUeฉTY*, ฅ–•ฦEฅQซ[€ฦิ6@ฒ‚… @*Ÿษh‘โSa\๙น2[2€4@!,g;฿ๅ/)| ใQJ๚ ๛Bขผฺ!ผD@MMP๏[w€า ŠB+ญhญP+จ†—+Nแ&๛น8งคTŸgไN(z†ƒ’ๆƒม๑ะuะ5ำ›#px ใ ท†ฟBb?{@๑ปc f๗๙๋๘_k๊=ั๎่๎†์ป_ฉ๙๕'[๒Oช๛7ีทhีFั ผX๊ซหยผพ(๔๕ฉQg+ำ. 3 ตFO๙ซƒฟ•7ฯยS$พb฿g๑ ^Žƒ4)pภฝƒcๆ†iร๐เ๕ƒLๅฅ๔ƒ๐^กงV๎>Œ€€ื–โ‡ ฿!™VุฒsฅR๋•Q็'FŸ,*ด"@kถ{KปK#ลˆ็น e๙›H‰๔ฑ๙1‰ต} yฮ.€‰กPnI‹D็Kฟ)ส?ๆ ๐๋ภ=่ฑ™ณ`-ัnำภอ]cKd Lัnิ๒Š@จห Q)ีฎiี*>วง๗_๔บร^s_๋k้6$๘ย ธ‡ดง]g€†ๅข฿่๎k~ญฦ)ฺn๖n๒Ÿฃ๐ฑ้ŽรูซŸ<6s0ซŸlCิ4D๛ะnK๔pK๖ๆ-57oศฝๆแ6ฐG*++cิEaิ‹ตQ/Oบ<ำjตะX FPx๘k7๔๑™๎6‡mไ่yโ‡z๎“ฝ~L• |ุ$f…ดพB‡เ†แ’๒11็,*ŽLyแ๔QŸ‰นO ผQ€:"ž๏5เRท”ขR๊diิขาุ/#,ถห กพนญqW[4/รw_ 8w€3:ผ๎7tฤ— M3ภ‡พ;ภ๕Xฬโผ‘๑,0€ฉ’ขดฆ๘๘-ค_•(KR๐Qๅ๏คiZV้mญ‹;$ุo-5{(ฌ]ภ๚ฤจrก”AhทŠ์x๊ช‡ถ๗ฏ๛]ิV‡๒|๙‡7;ืp๋๛wน๚9๏t)ผืนMC=xpพ^๗#u๑h้,ƒoฟi}๛ฑณ๔)œ xธDMMฐ฿‘>฿‘ฝ{Oออ[ผฅๆ๖ ูอ-Zี ญฦ‹•ฦื—F๕R๋ืืFฟผา๚โDซUฅฑ*บ๖จƒฒoa—ฟQ“g‰ฟห)Ÿ~สšฒ‘GlŠํ…B\…๎3d˜ฤˆ๑ค>ฃƒบ ิŽ๛c่PŽ๛6v™i„^๘.Yุnึฅ*XTJฌตZ-•Zฏ๔๎์yณ…๛ฺŽ bc๏ุล… `่kwaพ๒๗•2gฬpFM p้๘็ƒe™2€<แ,oŽบสQ๘1ๅ/ล€€Œ๖“@ภZขถw๛†่๖[Kปญ%k;•nๆ PKญt7hช๖5๒วŒญํƒลี„ะซ”;v ม› ่@€†v˜@x(ŸผwWŽp“๔ภY๚Žฺงfx<ฐ๚ ./9ล๏ึ๑ทึ}gํฟ๛อ6ฟd๋_c๋7ะ<€ี ะ๕™ึ็_หย|๓ฅ1_ฝา๚๚B้๕JcU!–Qซแj~„Ž฿๕แu.†ใœ‹J*??บnx,จžwฮฝaƒ‡ฅO้lvถw๗J(P-/.Œ. ฤำตึ'kญ– ญ– ƒฦ(h‚}Mด5ะaPฒŽรs„ €Ÿ6‡ˆ91PKŽ!กพแƒ5ƒ€@f0M$ c๕ฃsุ€๐a•t๛ำๆก†ฦRSืํุแ๖ฐ5mภุ ZhิQiท์Œ:ภอ†w ๖0~ชาVฃํVm๘ŒฝE๊W-๔Ÿ฿ํ(5 u} &๙นฝ๙ภถJŸlCP๏‰๖[ ํู๛[ข๗oฉy๛ซญ฿Bอ›ุ๚ฏิิ๗h (8]i|ฑ.๐๋๋B๛บะ_ฝา๚ีตาgงJ•…BฅtทBฏ–ฺ?ทGึx$jc›6A๕K2%mดPน{แIๅ?•™`ฮ`†pw>bํ :ม›฿=ๅ๊f "j…X–จชRน‚›h-ึˆo๏๊‡‡ฦ๎๗ึb๎€pฎN s3๕รUฉ๑อG1c‰ป–ฯุ =ฑkVžฬ“ใ;r›๒„iRk๘c ฅ&ๆฌ‡Dืgc•@ะ๏NGNมํฌmjวe*@ฅPiํถม‡‰}ญชjซ}ุHtฝ&ธใรค>7™*L7Nthโpฒa@วโa™ขฃํ๛ ~่v๏๓'๘ีผ๒๗่~Kํฎ}{gํฟกๆทุ๚?ด๕ป๛วำ์๑4๛_ปํ'จ๗7h—Fใื/J—๏สโo?”ลŸฟ7ๅื_๊โลฅึง'ํงanฟ๊ฌฯ฿Mdื›๗wn|Gนใi >NฬKำ#=Q(†1ŽฯCZ^ฺh'ชใ`๑ฬ`ฒ_d–tท@/o;0hว~๔i˜๕vD„(ฑ*.+ญสB)ญZ Y7Kป=ซ็bฐ0ํL‚wนฦQศf†$โ †B|Œ=x623รBXŠ๎๗ใc?ภค>โUมตฦ,ืฎQธตD๛m7๏vอ~o้แพถปฅzOP๏ˆššˆ,"ฃŠQkฅ ทr=Zใ‡y_; >ค๐ฑง๐‘‹๏ฟa‡=ษณ‡tฟs? ๛ก‚บ‹๓&‘?ูฏcˆ ฑDŸŸ๎จนyKอป_m๓ๆg[๒ฏ–๎๓ณmฟกฆ"U๑๒Lซ—g…๚๋we๑๗?—ๅท_i๓๊ฅาgจV‹๖ฃ/Ž๏ื> ƒZ0X11~๋1vœbRBV ๑GK–pู\i|žขƒrฤx~€BL๔ฬ…่แ6q*H๊ขŒw๔Ÿg&0๐ไDซยจbQjUฅ ำ๎ฐZj,ีmcKn็ภ๐Š9L€tƒใ”@ขC 5ภท๚๑ะฏshแ็Xฯšxฮ wคŠ)๙ิƒŸR๖œย๗ใย|๎a_R@`ิRํ๊พ@n jZF€๖;ขณ‹J/ืFU ฅŠRกึชะฤนศ่ศs๘#ัPS วหกย๏‹B๎่#8ฯ?O๗ท›u“œ•O5 ฦg๙“ฎทฅ๚๋}ห„l7d[ช฿ฺwญŸํฯถํg๚SK๗฿ฝ'ป฿- gF}๗บ4ฎ(~ฎ,พฦ˜ื/•น8CตZ"ๆ@ํwŸ๗๕>งูปZ๐1ImL„MฑฤดNyqiqx6fsูุ“ณR/ื…ชบ$h”T’7Dฤร*ซvต?นqณŸๅ็=v.ทฎ™บฝ[Cm๚pว_j#ขviตตถr_ๅถ๊ฑท๏ฉy๗ีoiš7ฟิ๕o?ื๕ป_›ๆ๖}cnญm,uฌ: ชN๙๙ช4?|[™ฟฉ*๒งฒ[cฎ/@ฏื๒W เภ… D๚ŸLN฿ ๏ึ‰J›SสRYS$Rเ่IJaอ†ๅาJทฌ"๑าu|%อadฉEN‘Wi๘ฐ๙ผ฿T7ณ[4{ฯ่pฅj] Y€บถ@`_>ิvณต๔๖v{Kuำ—އ~อIใwธ๏ เ„ภุ„?๎ [แส€อัค‡)็วฬtŸ|ส @MYๅ\šK<5ษปฮT…žSื#๖InŸIiBถ!hš๖kw๕า~ะ๖กถ›๛ฝ}ธ๛ฝืv๓ะุถฑ๛Qฝฐ YD]ืRหดฎ ุ๏#เ>xใํ50˜`ˆ}ธI ฎz@„`คfฏ`ฟCฺ> =ฝปiื๏ท>}[็Ÿ๕ง๏๕฿๛ฟ{ฟฯ?๊/ฎ๋฿~nš๗o{wcํ๖จฎ;ๆ k‹BแูฉV_ฟฎฬ_X๕็E๕ืสโปoL๑๊Z้“5`U๒wTฟ๒-ฯ๚‡ืณ9฿5{วเ€มม( ร‘๘ดฃ29‚L|h้F”1ˆ“๙|K^R๚Yn€เuNNœ๘:Ž@Dไm๊{hQOุn*… VX˜๖ษชkขฦีu๛ก}mรยฅ๑เ…›ธ็‡cHAค<ฎพ\ถทŸฃ|ส ภ‡ษ%ลๅX _๚Mอ@C(>เศ™ศ‡ม…ทหท5ุฦฺํฆฆ›ท›ๆืUกV๋RœU๚์|กO/—๚๔lกืg•ZŸดฌภb ชZ šฺต๐ํธจ‚ฯ 8๛_๋)šaW5@d มฺ†lํ|…-าv๖ž์ญm๎nศผkš›wus๓v฿ผทo๎ปษํŠทากฉ=“ฟ“ขPxzbิซ๋BMe๋ฯ‹๒ฟ\U฿|ฉฬ‹KPซ%aYjE][บญŒBฺ฿๋ิมฒ?QAHfbp›}œEl%ouใa"เ.r|ฤ]8€’๊ ŸฒF5“†c๚ปใาŒฌ lr:4‡P, _\ฃpMCดทSlษยfณm์fตƒ็CšeปAา2ม๐[าาภ˜ๅฯ9&!œp8ๅ[๚ฌฌ~_žHl)7€ไ‡Š)~cช*ค๛%ห,H €/าf~zŽLwjท3^S[ฺnธW;,k[- พS6๏ฯท๊์ํNŸžo๕ษyฅืง•^ŸTjน.ีbiTYi,JฅŒQh …Zc็"‘:Ž:Wต›˜DjฟฤgมZKMcม6 ีตฅz฿ดฬฤฆฑwตฝฟญํํอพน}ื*›ท{{๓n฿พ฿‡๛ฦ๎ถ–šฝ ลxฒึ๊๕‹Bue~ฎ*~ฎ,ฟส˜—ืจOึ%๕ดฟrณ๛=ฟ;บalฯg(]6dฆ™*าตๅู'ˆ*”ๅœG~*‰ปwœ๊?ฆํๆ_งP๏๏–ห@ีฟิpŸํa—ุ๖)Jh?็ ป]Unถ–๊šเaุ๛ฺ๎k ท๗uS7ไž๙˜!ฬี|cรีาW*'iœl O๑7pX-ๅƒ 7๒F=ฟ ฯGž๐%ผ๑แh–R๐ฑ4ฮฺ—?7 ๕_ๅ๗ƒ/1V@ แŽi&พ8gท#j‹๕าnWำฮพ{s฿,ึฅZ. ตX•jน,pฑ,Tต0ชZ,*ฃสRcQje Zc๛ฝ๎ณง กc;ะ๎าGาฒ 5uCuPฝฏiฟฏiทm์n[ำvSำๆกถ›๛š๎ปeŒ๗- ุ<4vs฿„ฮv“ฃˆฐ›i‹…ฦ๋‹B้›E๑ฟผ(๚CY|…1WจOV„eIจ5BXCหฐชai฿ฤc;่y‰ู ๒a"ษ(‹s„…Ybฌe๎i ๋Tึ~N–3Ÿ><ฺaI&uBไช"Ÿ ภP๕อ ีnTUˆWFื๕ข$Kฐ5๖ก&K๓Ÿ ผฝู7ึ’ค ฦ?ž๘ใ^h…‡Š?฿€ฌ๔Yฝฑt!ู๎ฆJ3/8F๔ณ—็x|˜?n›๐•rคJญ@jX๗ทษ”\‘xฐึาvท‡}]7๗;|งUำZ๙‹า`Uฌ*JีXVF-h731ํƒช]ิ]ญ[ตo[ๅoญm•พกฎฆถถอž6›ฝ>ิv๓ฐงอf฿*๙mM๛]um[zฟi?๊c-‘ํiYพsดB\T/N๚โei]Uฯ‹๒๛oL๑๊๕ษฐ, rส฿ปqx0zๅ9yป๘๚Eแ”8๕}W๒ByQ๐ซXๅผ6โ€S@NงH Vฆw๏|wป'ฝ{ภ/นฮ rZžฌ•B, ภรึฺป๛†šจฎ 6ฦึ–3B…(ั#พ๒wภŸNl˜s„้1ึฟ๗@ค>๔,Aภ็Rผ›;†เ\RtกUŸ‡› ญ€@‡้ทGb จDาH/}ชฯ๊fโvใ=ฆฃR`t eU`UX- UV-Ptqฺ(4Zาช๒ vสŒศ}•ฯ’mlงะฺ๏ฺ๏๊๒฿nkปึดk?5ตXoถ=ใNA€ชTpunิื_Tๆ‡oชโO฿”ล7_ๆ๕ตาgงค•ลย8ฺ๚ฺ”๏๏•?zฦ๐๎N๑ะ_FR&฿๊ ฌ๎G=…๒G}นธZ^(vŸB00ุ ‹ปฦ'?Š Ži€ฒDT uำ๐ขผ{h์พถ๔ฐm่๎กฑฅzทณํ๋!7<ไ|ฺ=d๘>ฯDฦสฟ โ4 วPwZแ๘žC<ล๏ไs<Ÿ~ฺๆ—”ธ๏ดbโCฅ๏;…฿e„@@ฒ๔CๅŸ๓€๛qSๆคxๆิหาง'kao‰ฌตT๏ํ<์@mํK;‹_#*ฅฺๅ‚ํ~&แKhtL@gลืšฦR ,ิuCMmฉn,ุ†บPh๒หญ5ยjฉ๑หWฅ๙Ÿ–ๅ฿~ฌส๏ฟ.ŠWWจฯNA-*Bฃ–ฟนOwX2ํ/‘PD7ฅnO&xเา:€~œ๗๋ศฃ๖qx๕ฉ•ฑ๓ธฮฯa>I๙ว†$ๆผใ:ถงW๖Ž๎วaัณ0ุVภŸˆšภยz…๘๚ei๊†*"€๛Mco๎j4ฟฝ฿ี๗ตSคาƒภนoHม‡ด|li`#œ‡›y๘sTyแ๘,gกa๘,ๅs๙>m.@8 IงF7~ไŠY!]*}nŒ๕Sn_Fฑ-จกฆฑด฿ถ3œ^ม^ฟŒ>๑ห4ฑW็ิ:€ฺ๑ฏ;n'๖งIฏบBmฎ ฏ/Œ๖หสๅO‹โฯ฿•ล—/ต9?Eตฌ ำ*_้ึ๙wํj$ๅร_ฟวฅ;!๔I\ฆพ”ห&) LLพ S•ฟ’ฏ ‡M'ยN ยn๘๛ล>d๎ีซ WG฿…ๅ_ก€P?1๑๒\klWภ๗{๛๖๎๗–kiฟทฐฏญ…qCค ;ฮW๘Cลงธ| pภฯ+อ‰๐ฆJศฌไsนย๙ปฆะกโ็Pง ๆs ˜taดwฬ‰_Gฟ?คด$O๋๔ฮjqฟ‘๚=ล‹—e™ยzฉ๑ลeกฟฒ2฿]™๏พ*Š/_syŽjน UํJ9ล?ถน›๊ัวต&'<์MdOฤ ฌ‹ฑพXฅœฃุงคaฎว๚! b}ณ98w:@ใ˜™7ผTุ4ีๆ3P-4l^”ๆ‡o—ๅํ}m๋†hปท๔ฐตtsป'K$}3 <็:ุ76Bๅn€ไ'jๆ8œบ$D๖lๅsูˆ KŒ8ƒ๔9iS้rxHฎ^จDŽ!R—o๊ร.™Iฉ{[f,อ”ฟฉ’1eก๐ลei~๘fY๕Oห๒o?.ชพ-ŠW/ตiท๘%0š@+j} ๛๗‚๗Qคี-ั~a-๑๘' C&‚ซŸแ[žฝB nYr_ภข๗๛ทVร0๖Šน– ฎ“x”ุ˜G วเ หฑธV'/ บ็ฒ%4ถ๓r,!ื๖ๆฎถฝ%k,ๆไพg\:ฮ_ฮเšสฦ†ฎœไ๊Ÿch}๒น๗‹iร/)ty๔ศQS”~ คฺ +บวิ5-๋1๒˜2yดF<]๕อ•๙๋Ÿ–ๅ_ด,๘ถ,พzmฬๅ๊EE 5b”ปl{฿@?๚ |๔ฑ?,<†q๘ฑw„}šฆแ๕๊ร…]ุ๙|Eฮ(าŽ€…‚๘ฎR>แ/ๅฦ .œ}ค็ไ;Œ&x$ย๋สพีฤซnn€…ฦ(ด่๖พฑท๗ํ,ีฅ}mฅ+ๅh-“•ฦWืฅ๙๓w‹โฟผช๒]U~๕J—็จW hŠ@+็ภมvฟแwcฺ+ดoI‡เภO?๊้'ƒsไรF๑~0Nyfl๑;ŠฯSพ(+กMag๙ส?ึ้N้sa—ไำ›ส•ๅˆ‡ึจ•ยnจkขาฆก‡MใฆะคšSY๑วXIแ‡฿,Žiศํํaจ|t๒ฉ'9ๆ€—7HJ=ใปกQ๐ึ]j_L>ฒ๊ดีฬD‘๒=z•ฎทZ*๕โฒิ฿~น(๖รฒฏ—ี๗_ล๕ฅj•aอ~ิม?\็ไว!@ัkํg€DฯฐOฦ๓qiQ8๖ื=๕Yส>ค๕ฅ_—–aุฒc #T๔ d&„๐ุPิฃ)ศ;xj3ญXZbผน…Z)ิ ‘บ๚mw–ึnไ?†า๘™จ—R๚œย็ฌ)nFJ฿ี‹ ๚๊cVฝ๛"”ภDม@ขN1ฟเ†๑ฺRโ˜‡๑+พลด?_VL#u็Zฏป=7๖{ขปMc7[Kป=ัvgฉn๚ วพว™ศ€SเŸว๊’/žย๛จๅsa๘K>Œฯฑ๚!ใ:ฑ0ฎพฑv…’š๔ฆห Oฅ:vช๕เสสfLิก*žŸW‹โe]‡E๙๕Eqyฎ๔j ช0ญ๒ืzHป]e€cc3W๙#Œ”ฟ ๅ“สg '†สS`ขว>โ ‚ ๘๙ภซwุฑ9 ภ๋ `๛ŒU>K้ใพŒืkp;ฎ€—็ ฺOk…P7@ัพzุXบฝฏํfP์ช‘8*•๒ืO1ท็น๒Yƒ€ฯฤ‡พ”e Ž I =‡†p้แšRฟ‡ ฟฑผธF,>ฅ’hมฒTxvbิ๋•๙๑E๙_?.หพฉส—WZŸฌเ`๛>`_฿๒?่Ÿ@ฑ =ำ1ๅ;žา($เXiห˜ฟ? |๊_rู้Uว0ิผ๘่ฃย๛ส #.'Iๅ๏ย้๐›sฏAHK|\๛˜"h>มMCP7@uCtะะ๛ปฺn6– >]95โb˜š ฃ็น|N ฯขๆาHov‰฿”ๅฯี)Vๆิ>๐%๖"piSฬยT7ร1u  ฺwซ…VฏฏK๓ื‹๒/฿/‹ฟ|ท(ฟ๙ย็งจชฐ่,ๅ[ˆเ่v0*~๐ฏ*ฃปŠฬ“™ษคZ:ฒ|นt8ฌๆ•NจC€ฑ_ษวห^š€‰ธ   8ฝฎฬอเzึ?._20_`ฐvศtทฒ+”Blท ถ๖๛ฝฝX[7–˜•S[3ฆ‚i^€~์Jƒg'Ÿศณ™ธ7O๓[NฮK” Rึ?DโAH{qโ/ฬ‡p ?Eฝจดถร%่ึ\ถฟฤเn1J› ฑaxRฉ ๙ุดAX&€{นIx‰MzXj?e๑3JŸsD'AA{ฆ>BพยgŽนrบ3Tส]8I™2ฆ๏`F šcืˆํา@ุlษ>l,mwํ—๏75ํk๑‹S฿ =ด่%ล?uE€ิฑA$?yyN€;O•‘หJoฝฤLญืT& …€าสŸ2ำว๊!…ๅฮศ•”้6Šื ฑ*žŸ๕๋สืซ๒๏?,สฏ_syฎ๔rh ทoผไO!๖ตลco๙ศ @ฮ)ฐฐฦL๘@eไc{)x,9Fภฏkิฒฮ™ญ/๘C…ฯึาฯA;ุื9ธอ|ยcงsˆo?‘/ ssS|*Rคจa\ทขษ5 Aำ ี ัอ]c฿์›ฤ„ภ\ใ$ๅ“•}Œ๒ฯู5ะ๐cZชพฑv~า๒€ หๅ๛8ณGฒ๐!3.gJี)&1?[ฎRO!็#z‘ฆผhS$ีoƒฐย(Yก*L7้ฯ[๒ง๔”ล?ๅ๖ฌM๘ส‰ ใ,๊X˜ิr†Y{%‚e18Oึ+๋ดฮ?ถS๚ฬ:ัฯด•e‚v๖ฯ0ฝ1{ฏ kฅ็21!&oXn—†ตRจ5b?4๖๎มาfg้aำุ‡ญ%๏“มaวp็‰Š๕ฟนŒ็ศU”+ญฉํ๘ไไน€0<ฆxร๔3! ReฅŽไะ๊9๔=€bไฟp\Z`ย•ต˜ylฟบfŒยห3ฃ๘บ๏‡oชโห—ฦ\œuพC ๐`; €8\๏|โY๖QŸฎ…ว„‡u๐้@iช <`ุYพี.X“6ŠXโ1aแ ๎Kว>ลa< แม๑ Œโi_1?‰plภุ ตBฅ ดูZฺ๎ศn6ึ7๖๖พถ๕a:@lโŒา๘cIฮ†?_ฬภ]Kชหณฯวค;๑ภ”•บnN\ …NyPใHๅี๋&@K4ก/ูพEl•zฉีซ๋Rต๒ท๎หฒธพิ๚d…X€Jตเเ็จ8(๎EX::PJO๓๓Dิ“รผŒ•=๚บย%’ ถ๙O่๕1฿#P๋ฏ/br”ๅ๏ฝV#Ÿ?๑ื8J]‘|ฺ]พ ะ>ุึิ €ต@ฅท7{๛ถถป=ลoNัsc\ŒEœข9—@Œํ„ศox|ฤไsNRVyŠเสแ๘@้:9ดX๎Cไง›ŠZs,๘c|eR™น{ฑr™œ์cฅ–•V—g…๚ue๖งe๙ทๅWฏ sv‚บ,[๋_ซvยŸ๖ฟฟ๎ฟฅฝ งx–ฟค€คฺ…nbหเบำท‚ฅnโฌฎ^_|/(tv{฿TZๆZ#AZฟ„๖Š./M๔ฉv@)cฒ๒—„ก๐ฝืณWษ$' /Bd "nvd฿ผซ›ท7{ปYฒDAวๅตŽฏ์dืฅ4)0ๆHฅ ๙คAภsแฑžฬ/$๒ฦยrโbJ_๒k๙q9จ๚ๅ/ี/ีฆ” <5o๒~h…xvj๔๋•๙ซส๕Oห๒‡o-Wหึ๗oด7฿MsสŸก–@๙w—็\ฑ'&>uํ?wŒ‰xเiœpK๙ยธจE๏ฏ๓Ž%๋t]Žaaฐ:๖ศ=y  ‹—๒1๑S•Š๚ฉฐ0ƒDb™cW‚R lv–พฏํ๛ปฦnw–vตฅ}=@Sbสจ–๗ลุ่๊Žฝฬa3?KyN@:—†ๅศต๘ฅฐฉyค‡5ฆ„cT}J้ง(\ห?วยŸา๏แนฮุ|ฦ(๕โฒะ๚ja~vY๐mU|สgZUกาcฟฟrk{ๅ฿ม๚๗A€`ฑOูฆVฒ่}6!j๑{อวิใๅƒๆ๑N-๛cˆ…ฃ๛9@Lˆห x๋?ึเะ_วธŽu†‘p2‰8BŸ…n:๔ชF…ˆ…ปkถถXะรถqKRะ6ืi๒X` ศ1t๖ฤ^ธONžรrฌPHคOยนๅ็ิER๐\ผžC๓‡/w๗’วฬอ˜ส0S“}ืว-*ฅพzU™ฟ~ฟ,]U~๛EQผผา๚d‰ช(4RK๛๛K+’โg๙aภุฃ”/=:(0วQภY๘aŠ= ค‰RY1๋?่ฐ]’˜ะ‹‡i๑9ชะH)ž- HW(rัu „vs ฤ}Mดlw`๏j๛๖f฿ืฤรci แ*’รLeRŠŸปพ;–ฦO^ž#รSVคTึฑเaJ<๖I”ฟ”/ืฺฯ๕•ล†งhสeHR@T Qxvjิ๗_-ŠฟะZ_ผ0ๆโLฉe…h4"ทใx;ะเ,w8ƒ์3๋ะJกI8:ศ ๓Dบ3YO?Ÿp฿BO(เHvyg@F'ฅาNy3ๆ๒yฬทc@ˆภฺ๖=ฐp{฿ุ_฿ํ๋›ปฺึu;p<pTrข#ล฿ุ\€โnิ˜0ำž…๗ซฬ๚ฬ๏?พไฎ(Ÿไฺtษข๖[ภG์“tไc+Yห) ต4ม/r7‰๕ฉิ€@ห๎&Lฟ™ว(a๒^1ทฟQ>๒~ƒศ(Pศ‹โำ๒m j฿ญ!>4๖อ๛ฝฝฝo์foiฟ'ช๑cAูW๗ŽScS์ห€ฑOO5fbฬ…๖Iศs๙ เX Y 9u9yJ่5WแC$”>ท?ธ๐”ษš›>็ืเzeิห๋R๛ๅย๙ปE๑ใ7U๙ล cึฎƒ‰8Tช๓๗พ๖ื)฿ฯ)ภˆ)Tโคฎ๐[6ฅ๛ฅ8_i†ซsฮ๖ฟa<ทA‹ฯูะปv ๘ํ๐ญˆnล‡–ฟ๗ฺˆชEŠ๗Aฏไ#x?๕ถนถ{ภJ ขยvo€๛-ู๛vB`ปS`MปฝฉŽŸย^J[วAฎ[ ึQSฺ๓ัหsNbŽSฉ›”ม1@ )s๑้.,?งryฮ$ย€9ฅQˆxqfิ7ฏล฿.ŠพY”฿~Yš๋sญ Vด‚รž*v% ๚ส€>,๖๚Ho…อภฐY1\)1B—Ž`'ง™๎i๖ฟ฿ฬg|Yภs09Jหี`&[พB”t‚Dล๛๙….•I๑โ๓>$ต%ท้>p๑ฏ็€v>Z ฐm๖Dัอ]m฿ผ7๗›~EW d.&Yเ)–2w?€cฎcRฝšk0~T2€qxjิRๆ9ฦsๆŸr_ด‘€งUผ=eqแN2)P9ฟฦ(|qY๊พYš?ป,พ๛ช,พxกอ๙ฉาUแถ…ะN๚ƒฮ?œื–Xแน’”z ใคฦš\ๅ0Z฿žยU’ฅ/ษ๊O๔ฆ—ั7M`ฒ๛ษŒฑม‰จา˜ื*ชC๊_› ๕๊ฆH='๋๚9ุ4@๛aท'zwS_฿๎šป‡ฦZKไ‘XืเjB ฒ”Œศqฤะฉํ๙จeใ๐”u™SvN’”ฑฎ—7ๅxJส?eฝว”ผ๒BคLา8} หJใ—/Kื๏Wๅ_พ[”_ฟ*ฬี…า๋%(ฃi4ใ฿mำฮ|†~ืฟ^HิhษZJัKzฌ !l&ืtI ้1vํT[ย}"ณ๚“ณ๘=@!)๑k“๋ฯธ‘ปjฒ ๗3Qศ) $€ยน&ยt9ํช๑`@ม"ยพ&zwS7ฟพ7๗m,ต$/[ผๅ˜ธzส ญ๚ว*‰!•ไ“3รนQ๒ฉ˜ส=|(ค—(ตc)S"ฆ์s€@ฎี‰ฒรพGญŒA<]๕อ๋ช๘๛ห๒/฿-สWืฦœฎA-J@ฃ”ฟ๎gป]B๋ฟป„dงฌุ0-ืŒuDยุฎขœ.…Cคkr.‘ๅ็K๚r๛‹cฤ> •ฝ๋ำฐฝฏ3่างaqๅy–๛ภ๒สฅ *2r0ฤ ุ\f! "@็จkขw7M๓ๆพน{h์noกฎ-ณPธ rš’๕Ÿณ4ะ&ฎ‘ั9aวzb่ 2ฒ‡2า…hUบแฺ/<bาCฏ‚c—{8]],“ฮ…Iฺ+_1ื๓ร@จC ^:%ิ3์฿)๗Hฅ•ฦำญ.ฯบ:7๚โฬ่ำ•ยฒ Tช]๗H0œุุ็฿Y;ลฎ5GFฉเ่ !ฃ†Lผ๋_ƒ!“’C’W>1ŠำO#พ:Œ•อ๑[.ญ๋/๒๚อ•vEŸyธช มp^ƒP๗v:{โ~„ว1 >า็’็,|6ŸO๋‡ีIk.mฒ’~฿ั๘ฑรถŸ•จJฤำตRื็ฦ|๑ข4_ฝชฬ›๗๛fณkhณmlท"`„" แ๘Ž_๎ ‹5ฏ?6๙ใ๛ ั๐'ซ่%yŽ@@ๆ„a‰‘ภ][1วœปxล•ช@s€@3e„€ภยแe ฏ๋๗‡๛k`๘โJ“oยบHh~ช๒ฑZฌ—J]๚๚ย่หsญฯึพƒ#Q€IDATจ– TEAจŒ~ฟฬ/๔)ปD~“G็^5็Tบ9lz8~vฟ„Lgขa๐dฦ$.Yภ๔ ฺ]&hoฟ‹Y\ƒ!/nๅฒ๚bศซ ๋ษ‘อ>๛‹QŒ"ๅaลร’Ÿ f-|สธn†๊Aš—ษป7m8*„‹แtญิซซข๘๖‹e๙๖ฆถoo๖๖ทwป๖|ฏม๘!วฤ_ LใสKีA2นLฅ๙hไ9บ|ม a๛ุrไก„t9iย0ษ%Bธ๔๐ๆX ฦ/.๗"syคนอภrกิ๕eกฟy]™?}ณ(~ถ*พ|Y˜ำuท๔ฯญ๖๗—A ๚Y,ํ2UอQฑfŒฌpๆ˜PH#=^˜8๗ปQ่าc\ู~~ฏฏฤ•( ไใ>,]+UU€ฦดณล฿2่๚ๅ*7ื-AXtเ๘๎H<…i†€ํชฆ‹T6Jปรึ๗†V๘ภŽr–>ฆ฿,„ Az ฏ ฦ๛ฮ%ฃ  N๑KูBภเงhํŸม ีe๋5ๅ- ‰t๗ขCcQ^œj๓ล‹า๒ฆj^^–ปำuก๏ปฏ ,Q๊Aแุลน65Lc| ฤ™€ฐŽS_ฐZž;H mแ๙เ‹S>”?†L?\ขิร:JJ?’ธ—@Bื!ŸRแ๑T _า>ฐ,ฯOตzyUจ๋sฃืKT…ิช]๚ื๏๓.wภภโไึ๗Oฮrศ‰๗ˆกธyวฤคkษ1Ÿึvyฝ…ใฐAผหใตฃ๐Sอq‘#€@มuqเ*‰x็๊+ฃ๐Xฒ๋rๅ‡ ‚๛2 งอuD—Jœ9๗๛>h"ั€ง+ญ^^‚๙๒Ei^_Wๆ๚ผิwฝฝฏ›ํพม…ส฿_๑ปtแ˜Sš ผpŒ๖8้ห' ž;p’2ฟธ๐ฉ ``น๒8๋W๒3 )๘ฤํ~น1ห฿)~฿฿๏ฟ09J?Fฏๅ ซ๔]€ึุmkิ‹Kฃ/ฯตZ-T๗มO—f๚ภxณฮ(žbPๆSš#)ุ#™lฏH=ๆFชถMTa<๒M์ซL0˜8ชฒ” R– Nู3ษ๙งฐ์ฎ€ก๕๏+qๆ1—tqฌ ฌย็ภF์๕’@E์5๔A ๚‘ 5์-ญ`ต@uyช๕หหยผพฎฬซ๋สืv_[ฺี–: ็A รyN.Lš (Y๚Hn2u’^L?“‘ฤE| c 8ฤZาyJ!๛ฦถืไ€Hฬ H๗›DษR)@( ซ…V'F_}qช๕ฒB์7๚๑ฌรฬ่”๏€ก๕๏Y€@G…‚|1%ม™อB8Iกฦี—c=ฝฎO*~วณสณ๎ฟFh๕๛3รr๒‡์Šชข,@ฬฌย‰ษ3Jสผั'฿Qk?หซ›F#^;‘V+ยช8Yกบ8ำ๚๕uY|jฑw[7ท๗ตฝ}จ%wา โgรฯw)ฆV6I”ฮD@i2 ืzฎg>9™ภAค”๒ยžพ๗หไ('?ฝยpN๙ว\)ง๔}เ[i ‘0้šเาด'ˆ`Z๋_ฌ4žŸjuyf๔้ZซชBิ๚เึ?|w ”€ป3Œ&๙5ŠฐบŸiฮศšŒ>š! า'o1คำ๖OxะœRฯ9ๅ?HX้ข’ฆ ัU=๐ ›ฤPPVฯล$็P˜6๒*Rโ8ล"ˆuDฏ.)œ‡>๔ฐ•R ช๑tญ๔ซ๋ย|๓ลขํfจท;lา๚1Tด๎8๓ฉz฿‚ฯQ๚S@€ไ ๐๋๚I*}_f0–๗๊Ÿศฃ/7Šs”@œj’&rส?ๅHนภปv๘gƒzซ๘ŸBย;D @aืหv๙ฉQgkญN–JUดKBฃพณพR‹๏|ภxa+S~ลฌ๎BQๅ๏+—˜q‚ยoxV%Bฅว”ฌใเฐมฺ๎ธ๓ส‚$pudoFJV๚ภ*w๑R๑Œ•?ช“oแ_X๕ูชง^E๒๚Gจ:ธฝ4,Fแz๘โข0_ฝชŠถซ๏ฟ ]•ชื6ฦ๙ต”ฎฏ”9ซ฿๙๓ง(ะ ภ]'Ÿ%ฮ๊“”ไ‹4zsCS%ไาฉ เBฅ*bโBฦ@bB‰˜ฅเำqน>ฟอ์(ีncิลiกฯNด:Y)ตจ†n๓Ÿภโ๏ฒ~งไร%€p8v]4ะกLWFญLŒs–}J๙็VษŠEz:ฬ๎ฌy”/ล6ำSุฎ „H—x–?โ0 €Wa_Iเ%ดฮ w=๔}๎gdmI้†iฃึ|๐๊ไคew๛c๒๛&ฏNHhn๋ ฺ๊๊L๋/_”ๆ฿/Jsu^่“•QฝตMC`ํ่แ ฏฤŽ8(H3ฉ}ธ1ž…ง{๚w—๐"๑ฆ’}ใาๆ€เiฆ๐a ญ{nฃ ้ื]ำ‰dง6๏Ip˜EP มMป<3๊tญีฒRXFw;A;OภŸิฯฯ๘wย~๔ห?`ย“ษส?ˆง๐ฑ บำ)฿๐ฑฃXูaล8ไ<ฌ=็[ใ‰&Fี™†H ร=่ใa (\>`›,1๎Ž,~ุๅc8 %.20LK\"QศziิผdฆAฟQD ภถซoึJฟบ,ฬืฅyuY๊‹ณRmvV?ิvg)|C ใ9ๅฯ- ไ”ฝ€๒ุ„ด €ซcสจAภ ฆ‹4R๐7๛ุภ ’uN๔•~˜Ž‚xN82eย_ุาyฎL2O,+…'F]œuบาช* ใ €pP8˜ไŽ5Œ๒๗ว<ล“ {$eaฤฑƒ๓๑Žyy๎[ Z๔ก๕๏,v?_hsอvฌ€+หOหฐ&˜จ/xeQะงฬใ>Pา~๓:H Ÿc"ฆX๎$$LŠวHว aฝTxy ๚ลEa^]•ๆๅei๎jป฿[ฺํYWภ 4™€š็X้XฒS;†Lซd๙sํ๘่Aภ โ’หp้snxŠ๎ %C |(c๋gcŠ฿ี™ภˆเ๗[ืฯ๕ติใŒ aQi{เE๚ภbPFˆR๗‰ำผ!#เŽฝฟ)_๘‹ั~]ลxฉ 5…‹๕์*E`  pพ๎–พฌ๊_฿์šŸ~ิ{ษ‚–ิ8ศm5ฎalว>tฬ<€ฯฬเq’’้ฬyŒU<ˆ1)๑หๅพว)ษL‚p>ย ฌ ฯNต:;ัjน@4๚ฏ ่ืญ๗(dช้[ฯ“-0ภทX…qฑทรธอํฌ๎*๔พ^^˜H f์‹.€.(—cFŒ€ฯธบะ!Ÿ|#@ิชฉ = ]ฉ๏ฐแ$ฤ๙ Cช:%สzฌkจŠแdฅิหหย|๙ฒ*็็M}ฒาช(๎n้ยแS*ิ~Z ัฑนœr``๙2ล 1าHœbธ0งุ-“.’ ๐Wจs'†ํ๗ใP8ŸขuSปUˆPˆ๋ฅR็'JŸhตจP๕kzK฿‚oO‡ี’Vp฿เVฐUgนแ ๛•˜ ˆyดˆ+ฦ–tด‹3า†ึฦ(A{*3 ภ๕แ >,"ๆF่]‰y  ๙x |ใะ๒๗๒Šล3ฬไ–m,–ใื1.””บ฿lฝ=ZญP+ฤe…๊๚่/^”ลซซrvR่eตkš†7ด”h๕K เ…/อเฎ ฬyฬฐ๛จeำ%ว†วฬณœkp`@๚เS๎)ำฯO~ณภดqŽ๕ŸcมsJœ‹“๒ Žฑ 1ฐ*ฎWJฎหด๊๛ฦ=๘‚Hึ฿ฯฐีฦผp{๛'!T‘ไฅ%n์๑ป‹sฤวลลš3าฉพR๖ˆ฿NZ๑\๛[ฮโ K]ฐร@ฉอ๎š”1DG?k๙๛ส;H+Yœขg!ฏ\‘๊gส๕ฏKa<ืAธัjFŒบ ‹ ๑โT๋W—…yyUšซณยœฎMณฏ-ํ๖– ‰–์ฦ@€oฉวzฮฮ€าวbsb†G/3x:‰กY7๛bๅNyธ5๛กHึ?G๕svหcxIก๛็yฺ[๋ฟ0 •ยีBใษJแzฉฐ*•๒–›๘(sIัs `˜ชK’ƒ9ผ่)6ง8ึšฯ(ึณฬร๊ ญ~W_๋~,tม@A๏N๐Ž]ธฟJ€ปe~ฟโ ะ๋K‰cฯUPnฯ่ญใ,~!ฅ ƒ๘#‘‹Gฌ 4ˆ๋ช‹3ฃฏ/ ๒ฒิWgฅ๎ฌmš}ำ4–ภ?ฑqo๊ึภา>17@สBิOFfpœpจU —่กXœ๐ใ„Sฑ๔พ๒งศืฆ”pึ?Gฃ…iRๅ‰ข5bQ(ฌ*…ซฅย๕RซีBฉฒภมง‡Š(v๐ึ ทยฅfr"X๓ัsŸึ๗ส'แ‘!fข0>๓ถ๕Š฿ซRNึAS<00ฐุฝz๘สฟfฯเ๘:ฃ<เฅ%ฏ.9†Rm‹บ"ธXคc 8กุY )๋1*ุ่ผ€ห…ˆ@!‚1€ห ี้Jฉ๋3ฃ__Wๆง_๗๕ํ}=ิ๖~๎่˜ŠฑU9“•PF d"๋ไL›%-)<๎ŸSฦฑล–ไqk๕ฅฐX|ช.)‘ฌ{ยน?ฎฑ฿ฺ €สRแฒาธ\ด_ซ*ฤย *๔T9vŠ?Ošˆ9ใฮ”?SรOย91y !๗vw7๗އ๔rŸ'B[Gkป0๒โƒt>ฝMค๕ฎ๏[แ~<„๙รบ‡ึy œ‰˜_กอ}}นฒh\Ž,ซด‹m๒ูR๗3•มฟํปF @iW Tงฦผพj?|~Z่าจœqม+•—uLูงvDฆผœq*ว˜๙่dfžN$+=ล†$gJ\Zษ'ๅOndZ6ธv ฤ๊#ฝฬ~v๏˜“]aธfumŽ~y/A๚0|†ส ๗๛h`1hƒŒ"ž๔-/Žตา‰‰๗ยฃด~FxŽBงDฺ,๕$ =ยฎฑ๋gD‚Bžฌ”~yUš/^ิๆ๊ผะหJ+ฃ๋†-“ฦฎV9ส?ๆ˜๚'m1ภษGfภำหc_”’ฮ๙หHW๎‰YœวD^ @Dk`4โrกิzฉqนPXˆFcทะแฃ?ญพ ฌz๎O2ๅฤGบrvยMฑฉงุ๙คฎา….๒บ>ํ฿w๙Gดบ&=๖5ย๔aƒiœ^าชƒxH !q>”๒ฯๅเ!1๛>€ึซชซ3ฃ_^ๆ๊ผ0งkญ•VZฃ4~๘ว1ซ*˜สไผ่8Ÿ€พ_™€#ำo๛"!Fฦtนpeใaฦฃขค่รxNหลโไJบฮ(ภ„eฅqฝิธฌ–FกัุํGเฯ?ฬช5EแณUŠว†แยํํ๏†gํJ๙xŸEเ6ภM*ไ~ƒ6PPWe_‘๕Vwะnห}}y;‚๖ปผ0๚ภศ€0โ2A๏ึˆุXž๕๐หขqq’H_๗#&sฎฯนฎุึHZnไ๊mโ6^ซ๖็งJ_]s}Q่ซ๓R๖nฏฌ%ฺ์,าpO€\ซŸ‚฿0…!@@X'nL่eN8@B|ŒšJ…opฮlI๑?F0๒›๓—-F#vKีขิXDญา่ร?j8e™็To เ”ฟ„ใ‚cN‰‹ฺ๖ŽCภ0ธรA˜ิง|ธ๓๛*™๛pฏฝพ฿„^โม'ฝraร=ย‹z@'k„ ๛ชฅฬf<ปพf”x๕ˆ+ o~@ขฺน"ฉbw0W๛Pอ”Bฌ €“ฅางZ__๓โขิฟพ๋ํฮฺ‚ำ่œ๛FN#0…เ€ว0=F}ฌ2ป>ฌ$ฑ~F8E~#e”โ็๒ๆน๔D~รใิuไpl€(ฌสฮ๚๏้ทํ/๐Ÿ›กผ41 ‡ไHw WVะd’โยx€ฑ‹ " โฃŠ&Tp‰ดƒ ํ‡๛3๔EŠ฿ฟ.๋`๓…U๗ใฅW1๕๊PpญŒพไสฆXฺX‘™`vkมฐ?ุ+คnI เ้JฉซณBฟผ*อีyกW ญ”B้aอa น7ื0ล๗ฯŒ ๘่Aยฬ|x‘ฌ๛Tฤั๙ฎฏm}^.•๏•ี3ฤิ-ลฤ,}้WŠใ๚5v/๙ุฅ…”‘Vh^ัํF:ท ถ@”qUกบ<3๚๕Ui~ญ2?ถk๎—+Rธ"ฦ)ํอฬฤภ๏H=๛Qษ ~แ\R|Lkธ0€ฑฆ๑cสแกH(7††U๘น้มh„ชDฌ*ฅŠขืืี]‘๋฿ฟTธ-ฐ๔ืg“ช(6'N๒ฅ[biต{์•"Wบด. q)ำื€๗๓{๑~ตF๘ล ?ไ๗eฌ‰~ฑ9เ ก99ฺPLฬ ฑยอ] ๒๖)ส?บะ D์๔a ฎญช๐D๋WWฅyyน7'+ฝืvฌ๒—hด๕–ป ๐ ๖/%˜]ŒLq ไO-3'/1N[Jใ&๐ึ]"Dะฑ,ฺ๕…ร๖ฟๅ฿๋๖Aฑ0[Rค๐ๅ๚้I*ƒbส(/~เ`สžrทIธ=1ฅBaฃxŸโไ๗œOำ6bฐ๎ภuไน‚บWฏพ,ๆี%(‡k๋เ:~ึ žฝL ”x๕๔ญPn๎<Š ฏฐRิm DPภณ•V/.J๓โขะ็'F-†{ไ)Š~๊Š€ุภุDภ)25•™๘ใ$ว5ภ™”ฑผฤœง€ยิท;WCๆฐR๚Tฟl{ี~จ,ฑˆJIogL๙{ฟŽ ")๖ฐฐš!mฯ5‡1ฅู)๔มื|Z?ฦ8๘–qX>'‰ฑ8F"ŒกU๏S๚๛"HO”mเจภไฅ๕ฏ†qu็ฌ˜ๅœณK C _๊ะŠ็ฌˆˆึJ`H+ร‹` #1พ[J๋Žc _าข)fก?Vˆจ5BaZ [ุ๑ฮฒ๙r#ฟฟT]~ ช™ ไร4#…๎%‡Py๙i9๎œSฉGวSั‡^๖ษ$๏‡๑ก’u—฿]&;.z™Bบ_žู๖๕a|˜8ทง(\.ํัสŸ)—R ดย๕ฃ๎€๎เ!ฟัซJฉ‹ญฏฮŒพ<5๚tmิปถ ๎‰\าžฃรใ\ส?e๕ว…œ—๋ฃ3๘ด$x Š)6๒ง”z˜Ÿห#]or•j็”…‚าด+๓วฐศบw^ุจ9รULผXmIษสžw฿’ฬ์ˆ๒gใ‚mวิ[Z๓Lxสา๖Kh๘Šล๋?q[฿P๙#ุ\—ป7กฅ็ไฯ ๙ลrผฟ,ๅŸŠ~TธยผใnŸ‡๖.ด.ฅชิษJ้๓ฃ/ฯ }uV๊wทMS7D๛ฺNแ—r€0,ตe0ย4๊Ÿ‹Oฝ„˜ภว% ฬฆyL|ฎค”vNฐ-น–}ช}˜๊ๆŸVf?}wธ\ศ\)xงฅeภ3a•ธ[ศŒฝโ‰yzB‘H"๏z$;œโ็X†ิmJศ บ:pสv ภ]๎ถšฆ Ž๎๙ ปƒCึE๐ถๆ”ดค?–> 4พ‚z๐Ÿˆค8ˆ–Kม3/ม฿ใA)ภย,+Tงkฅฏฮ s}Qš฿ือCm๏7ฃ๛…KŠSฒc`€ห ฦ/~lŒL)๙ฬ“g %ฆp13ํ”ๅุ:@ว(ฃLหด๋]ฆ ฤ,x.ู‘ฒ๖9z+ฆ”ง๘]ศี๑ฃ—|’ใx ‘,ๅŸ*๓1ฺE,P! ึZ!\cc"๕๎J๔~ชKคๆpŠ9Œ•ดไ›ๆฌ1กVžG‘์กข”+๎ŽKKLsp\ึd6เ๐ !($ฐ@`โข@ฝธ4ๆ๒ฌะหJต@#mๆ0‰’จIส?gE@๘ฒด``v|œ๒G>09<ณ;Ÿย“KๅM51[โ)yฅฐำnภแ0๖็T›k๎ิ*็ธ บc‚Dฺำศ~‡แฃดฉ๎žา^ž„5อบB๊฿ๅ๓-f†๎—โรpŽ'๖D‹)ภt”ุwŒ’–2sŠžRiฅช็\W่ฎNOkkCwฌAU žฎตพ>/๔ๅ™ั๋•Veกrถ)J?T)ห_:ฯa>)๚`f>f๙ฝจงzxsXุ 3erส"jDl9ฤรฤtR• ๓ุ‰h9๗@Œ`าŽุˆซ€`ภ €PF6แฑYัมu$+_~lhะuA?bะ7@—ืAธ'b˜ฐไq*˜rน๛ภฅe˜ฑŒŒ0?œะ y;ุ๏%„ํlYwซฺIˆŠขเ๒ฌeNWF-*›ญEK–]ภา)‹1_ <ๆƒ@ŸŒ `f>n๙ะQŽฒMi9œ?7อ$0‚เ,„nใฟ 7๘KUMX๚„•Oay\|˜?_ˆ‰ ะ”—sฝฐŒ ŽbใSŽo€s45๛a*<—w4iฯๅ๗]๘‚x๏\สใื…ธ๚yณ”B๑ข…;ฒคKrสฮ๓๋!6!CษงJX FฎJŸฏพ81๚์ฤ่๕RซฒTแr่ฑ\๊ฑ’ฎ จdฯWr่yˆค‰ๅ(1็hY€€็8ิ&6fpแ™"ๅ1e+เˆ2\็`ฒPฮc!I„Žz-ง๑\ฺึ@มจ0ำˆ!พพlcr\aน๒!‘?’V‘ดฌย—šห40@ฟค๎%ะ *ื+ฅฮNด>?)๔ูIก—•VZฃ๔ฆฮsทด2 ล |๒”(ณ เy‰๔เฦด!dคใ์รฬน๙9MŠ53ึN|*ฃฅ]๙(ˆcห”\R;œs๘วศ+”ไคล.aศ8 จvŸšw้ด9ปmฐื&&ญื^vใโฉ๐6Š”{ฎ q.อ—ฮS€DKK2หŠ๕G~p8 `iVชำ•ึ—งF_ž๚Mlถ‚๕๏H๘‚ธ‡'Eวฅ16 UF์ovฬ๒Qสs๑Xy๐˜ฦ`,า;ฦˆ0>็ส)ึภK;šคปnŠALฐฤ?Žฒ๗sฺ›;' ฆ F.Ÿš๗…สiเ๐หเาROมต‰)ืฏฃ๏๊5ูฐ}tฬ๚'\ผWœห"่1mค ์e&๛ฟ-PHPย~O€“B_ž–๚te”ั˜๓ะ‡qRบิ&@9๑ฉอ€pƒ่™ภว-O๕PM-็ ๘฿/#}ภ\?o_/Œย4ฬซ\Sื‰QฑฑŒ+/‡˜แˆ€ั€ห ๑ฤ(ท ฮ(๕๐ๅษฅSซคผแฑ‚v๘แ—X}ร฿?Lfเใ”'š{ชหจฬl๛๕0kศฅ๖Hษ>UฯXฯƒฒงvkŒ๒‡ฤyฬ๐ฮฦ‚ค๒ฅ’โc๙9wAJ๚ษ8…็Zท~ดzภ๔aŒKaภDฯ์$r๑1ช>า็Iป,bed*w๑^d0+์‡เ&ถnฅชีู‰ึ—gFŸฎฎ … qสห…‘ใ(ศ าKศฝศ•ฅ/ษ >?‰ั๗ว>”วX1Kž>…u`Gใณ๗Žfpภ(TŠฅๅNฑ<)—“น8),ณ œ^ 2ห๔ใ‰/ืWศํ/-๛ไ ภC_Wฦภฑ๋’R๓ห™๔ิiPถj|wDDSKห)เ˜q5ษaแqดŽ๎๚4v›ญ”พ85๚tญีขj?ํ 1/^+) >๔๓หp`!ผฮ'ฅfภ็&SœาyJหP"๎ƒ)๘˜8ส฿Ac‰K`-@๛ RŸฒฦH๋"@ $ุ๋nDฆี$ค Y?๎ฅƒ ญฯ]+Hหบbuˆอ๖™ิvกูู๑!ญ๏าด?Œท ๓uaF@}uesu๋'uT์7ฦ…GX๎U@ @LŠิดAZ?=SvึˆpH€hก์6:?ั๚lญ๕rกUQ(7KNฃ;"ฝ9VŠศ๙๓ฏ)Iฌพ…ฬ ภ็))ำ6‡ฯ‘0xŠrขiร `[ฦฺฌS๏uN‚H0<—ย็ฮcมลรžŸ4,Mศ๗˜xŽolาฆ )ข@ัJ Gดยว(ฉฬฐ>I รด\Ybieยด ;ฑ€Ÿฉ}*  N<]k}บึjฝิjQje๖žCมศ๏ฑ๎€c?”:แ๘ฃ` f`Ib[ฆX๛ฑQ์Xึ 7้ฌhขฆ&ชkขฆ๑วขภาๅ,๙‘A์งch๖kกL|ZWทA•๋ๅ๕-~ย2๑\ท„#ภคๅ€Q๋:LภX๗Z๗ฮ๒๗ร=œด2u *?š˜ฬ12q)ชพ๏เหฅฎn!ภaWD /Jgย;n๎pฏŒ\”จึ T'KญO–ZญZmv๎ฒ๒  9TcR›ลGฎกฬภ žทLyX~๕…ฒ[Zh€}MPืถiˆ;ู…ป๐a๘'บ† โ•ไ(Qๆ >๊g`า๚mqIผผไ]‡˜kf3๕e๏JžR’GƒKำ_"่‡มw‚ธ˜{`ะฌ<๕ .ฆ๘Iฎ๗H็hฦ˜ีฑน|Kหไ™9แ$๚ตภDSล๖AVฅ\UJญ—Zฎฺmo”ฺํญ_in๘Oš๔Oup ภ7Ax]อ‹m๙]ev|๒”TSศคˆS.S YKT7๛šh_ี€(็:1/ Gฉ2คcฉ|‰ลฬฉk†ปภgAˆK›S_ฎํ ๏’H+s&ะˆยำrสฌห3RŒ฿'#WDŠโ!ˆ๖ง iนrc,C%๔อจ !;ฉB ศ&ข๎‹ž„…AฌJT๋…Rง+ฃNWF-JJe๙ิฐ\0R๔น ๏E่df>m‰QOนfํM‘กาฃข”?k ๊†h_์๖ว๗Y #ฉn๕›็[น]8kย!hRว ŒXฺภาญ๚ค๕žx|r๎*๋`š0hฆc|kบ=ะ๋๏[ลฟ†+ƒนn_ญเวะะD๋?š6r PHภ‰+‡mธหคoผ”0šฐ,— ฅNืFŸฎ ]•ปF!6A6๎กฯก้cŠสงฅq๕“R3๘ไŸ“`6ŠS๐aผ4RไJ™€OC>ไƒ$Y๏ภœวlIqsๅง@ยคสท €ฅํฎeศบ*zƒgง ฦTย)๏yฤ:๗gฉhVฮุf€—X€ะpโ~ƒ6$•„ๆ ๊ Cรmƒปtแ$Aัeเๅ ฟ"(u_2NxU0ิŠก•{MใŸฃqฅดR; ๙y`ก^ฑพฌLiWธmตจ ฤ“e ฅยเหภ’Ebr•S| ะฏO„b๛ใef>~๙-0MฯฝN๖Kำน`ณณด฿5ฺi€1ๅ” ADF`ชค „P๙ ึ>yวQ—คฤVrฟOฉง๔Mฆ๕/ัูaพ่“Hใใœ6สMแไฤภ U๏[1}h'}ฑ๖็R0ผ/ls8?@ศฤtฎ Pฎ€๋ฅึ'+ญ•RZ'—ไพ…6ๅOš+ภี#ผฮG'30K(ฉก๛uว9–~Xเฬภะ๎ำึ–hปณดูZฺ๎‰šฆH;e92tƒหqvฤhH๑ฉx˜:๗Š;ฐะp๔ากC– ‹สป`ฐŽžb]+Yr~’TL์ สศ๛๑&Bdศธk๙ฌ Sฑฒ1^\xU(—รP$ŽM้๐ไI๑c‘7 A DคM biื …'Kญ— Eก‘XD!ัXƒx๘X…Ÿš ึ๚˜ภ,ผp \R๊IK็B†ำFฌj&j๗ุ๎-mvึn๗–๖5Qc‰ ยvu9$wฯ๊฿Rv”r†/œย2ย๑ส;G8 ฌˆร7š7 ๓ด+ฏƒฒSŠaะFฉ&บI๛๒M‹†qmw} ผโ7rM„๑ ช]ึŒ‡cJฝ $็ฏฒ% ภ‚LๅบU’ซยwกPˆซ…V๋…VหJฉฒ@ิกฎ‰ฝต๎n€”J4™w,ีŸ’ฬ.€Y$IY๊าฑcrFˆ,ฉขอฮฺ๛M7 ้ท๎(•ซอ™ู, C%œ{เ$ๅgŒ ๒๒ฅŒ˜วH Šล‹Œ๓ุ‘ฅ๘…ฐ˜ข๊ฉtแUลg(l฿*็fใ็โๅ˜K€๋'Jิ‹rc๏Š๔ฌ(๓เ‡ฏ…Pภeี๎ะๅถ$ั—6ˆฯ๑=…ฟp๐‡หฬฬโ$eํKJ‘ฌt.M&)nˆฤMฉzeตฎฎ-=lบ{h์C7๚a{ค<2.้๋Kึ]ณœY~ฦcG\ฺ˜ฒ็า๔4บด฿>ม๘x๔eBฆOธc‰eษๅyP๏‹O๙้™ถ๖ ˜’hs๘Wš:@‚€~…Aฌ ฅ–•VUกัจ๖ Š #ณ่สp้ยpŽฃP`ŸŽ„ฃ๗๏q=˜S่~บ˜m๚ๆZ9a$ h‚ํึาฆฑ›†6[ขจiŒส้บ)หผ๋ภoW๋็a”ฑฟoด5/๋wˆŸ>ฬ)œ_ฆ D๙ ดพt็ณโ}ลO฿7 ”Wพ‰~<๏ฟwื สฑฺ9w-.o˜mhH} 0Eย๖ฅ,…„Z!pQ*\VZ-*ญŒQปฦ๕t๘p‡ว๎?‰)ฮ”ๅฆ _Wห„= ค๊jFQ,<์zw[w7ตฝ฿Xฺืงmซszึถร}ุsบ7จ 4จฒ8@็บฆ€ฉlw*(๖ส$ฏ หIต>œฒSใ"ๆ฿W0ปUpจ๐ "]ร)v6ั์pฯT—๊)7ถ๕ฏFฤgาž)nO€ม%สŸป"X‚v@ํ~ฦHญเ^XRๆา๑Wwอ๐Zฟ›eŸ+3๘8eฒ๕›(+WรI#SŽŸR๒นŒ@–d 6[K๏nk๛๖ฆถwํ‰šH้ ึวOร๐ษw็?นธ,ะฟ €ษL^ฎา‚๒NtRฮ ‘ุิc$uO2๎›฿ฑ"๛๔,งL9ํ‘8ฟฌ0^(KT๚‰:ˆ๛pๅrฌ†t ๆ๙pส฿;w˜m0":จ+ซ{/T็0FกRูซSึy˜…๘›+3Œ่”?ภ >fy,จR–น”ๆ)xฤ๚ํKะ€›>ฌ-€R %,บืปHท.Yวฃผ˜0๊ต„q\่_จSp`<๘ุO"ox vืAกŠ<Šฉ'Ej? แศ‹ๆ๎'W)68ฒ๐ฅฦล”ฬสโฆฌๅ๏OS`Cฑv ๅŠU"ฑ?žบ~]šฉ t!ญะ(Dฅะ&pฬ๏๏? ’ๅ๏วลhXธbโ>)™ภว-Oลฤ”ซคฬ!‘6 หต๐)r.ž{ั9N๛9w-ธนซํรฦฺŽศTะN"ฆH) ะŸK €“ !Wณ ง@€Ÿ< ืาง”ˆ”ฟ฿ฤXค๚!Hฑ‚" ฅGZศวnิฃ”V*;ยฑบd\WRAไ๕ใ ่nV06+tทปด๎ๆด๗+ืข–,{๗;E๑Ksb๎€^ๆญ€?]กŒ8‚ผ…KKB9Omํ็ฐYm'ุ๎->ิ๔ฎถ๏๏{{฿ุ‡ญตuMrฉิeท˜l);ๅึLRL$wœ3…ycฬงtญ#ฺ6้ฮ=แx™‚–“๒กเžฅ๐œ4sเWฦK“` €[{e„๑ไ 3ส|Y\ตผ๚๒4? ณ ๚ฆ‹๏ฑg๋๛7 Q!`ฎ—็ฃ๓น‘ฃ๙> 03Ÿพpึฑ G!}ฎ.ฅ™:9yฆ€€ฐœyž@XxุZบนฏ้mc฿6t{oiน@ชJo[เAnฆซฆ๘”“D ็มใ%+˜ธุ-ๆ๒J์0i ‘Žkะ,)ฬ/ใฉมภb–\2ฉ|R)ใ`ฐ/1i „sษ‚—ะ'ฅฒ%ฐึ๑m๏Mท$ท‘,a่๎ฑไ*•Jณœ๙ๆ฿hฆงซK*mฉฬุ}ใุ๗ƒ›ัh€‘ฉˆLs`ฐษศ…็"0$ทฯื C +ืkŠV€6เIDอ-ณ๐r ”Qศ€—‰^S_ด< คง ก฿)ณx?ฃz]ป$0bYy๏‚ร๙ชBlFโ—ัธV•r+4คยลด๐ุ๛*%ฆ)ฅก”๊M†๙Gำ EDฯร็E‰†อท0?V‰ฟŸ|(("$„ฎ;๋งิ6ๅ™็็฿—š‚3๙bRd่Š“๑hŒc-€5 €fžOAช ,ฅN("„l๘ถ#ิฅ$-ฝgวK„ฅ}™z๏ป๊ฦรแ่๑nื๘๋๛ฦ฿o/ฟ[ก๗๋ฐ€pWลไeฬ}ไ2๑D 7หีƒฤำ๖Sด๕”๓Rฏว๊~ ๕16BูSก]#V๕Lkfไk"๙bekจ๗ฅ:ดzAจW9'ฆLฺืˆ!‚FำฎOดaXGฆทฃ๒,‰‰ม$คง ˜“ฅศภท๔ฅ4I#ฟ‡น ภษ฿ uƒฐOX9ฮทV€๛mใฏ๎jwปmฑ ขษW†šฒCษฟ;F^ฺฟqจต3mšฟˆS‰ 2[ิGk Vฏิ`„ู ƒูtล”>Fาx])–†S :ย0แœr‚„ุฑฆ2+ืเBมpฏC๕*ื 3R“Q/–xiตc‘์az,;Œ0ฦtไŸd„4s`้1ำ~ศpสƒ๚X ๚I‘€ฏš–ญc ฉพ˜ฦŸj-PิฃำP5ญเำ]ํo๎ท?z๏ 1ี๚‘๗ๅศI)ณB้3H๏’6นCŠ`๘๔-'hIJัโiจี•ฺG Kฌ Q @„X’ิส•ว\›/X ดvI T๊ $ฉ้‰UดวD(@šA…าVhg๐0@์Lh=ีะ—[๔ฌ€ฏšZุ็…žุŸWถฺ^Ÿ๗‡"ษf๊ฦรรฎมOท•ปบฏ๎เฐช/=Œถลโ˜Z๒gB@์HฝแๅขJ!/~ถฏทTฏT^ซ @^คจoƒา1ฉ+1rŸY@Bxl~@ป5(…!cxสสbคlO’†วšซี#ๆm@%งuอ4|n˜วทฯ—Uˆp`:˜ dาโะ`x™งๆ{ึ„ฯ‘€ฏšJ๓5อ[ำ๖Cฺ๒๗v๐๋kยŒิ'hโรพ๑Ÿn+u[ป๛ฝ๓วส๛บฑfตc,;‹?``@JS@x%…ฬT๎5o๚โํ…Z—D๎าq(@xrโVปฃ”Bป“ฦฏj)zิ๘ฎิฦheไ? ๖๏ข]/สฤ๓cŒ” ๆฃzฺ$1(lะšุQ?1T Nd๑ืญG}ำท9˜‹๏ฉXขัŸ‚ง พ(๒,€ฏฑJe@)ƒ‘บ–XBy ิB;"/็v‡7๗•ปพฏถq๛ฃวชFtว5J่Kfด?ฮ/‹t๊…ˆฮjข‚fฑิŽตูสฅ<†P,ธC~ำ–œฟ‰=0[B]ิึŸ'n‡Š™vLฏ.x}š“v)้“*ฅถะ1 —ฦฉv/t‡ๆซqb_ขใ๘fด๛d ภห„dึาตฒB*อาฉ|ฉ&ิ_(<}๖้|๛iเ‡ฦ_฿ื๎ฆ› ๐ๆUa‹ยšขLฒ๔ีฆขJ๔c฿ ็ฟ$Z„<ก๐ึ,Kg๔ใ@ฎŠP ฆ ็I‹Y N็ว@4ปG:]ฒ—&ฃเำ”ฅyณtRje% Xฝส๒sูฆ฿กa ํฟ—™b@…aฺ ชƒW#ค`=[dเeB$=˜?x)ำMBคห—ส„ฬ๚ฉ?ญmษB€๓€วสรรพม๛ฦ฿ื๖ก๑๏฿ฌ๙ฦ˜ณUC๘Rภภษ฿Ž์š(A€๚๖๛$ษ฿O๗>๚I` 4Tšlจ•–วใ๘8Jc+๕™๓๋h็=fœฟŸู{}ั้ ฤภT็HYสดรฑ$l(๕E?๋ษŸ\nJ๚ใ'†ตษฅ;“?ื๒ู๙่ฑซg\)๘้I6V_๊C๗bศ ปพฤ8้-!‘-yšE@า๔?— ิWต๏ˆuใq๔~ื๘ซปฦ]ึ๎~็|]w_ ํ (8์e˜ิ‚๏‘้พIศœ7ำฎcฯฑ}ญฝหปฎ๗„บแฉ-นจlสjถm™MsQIg๙ZญšฌฝU8ฑ Zฝ+@~<"xD๔1ฃลใ•SฮyึAถผ,„ดเ%uะญ”ฅไ ๓ะ ฅฺ๋'ึ>-•ชCƒWw•p]นฟฟ)[b4ำš'ฃร,๔ƒ@`ไ;ม[I‹LLั๔ีแi๚Xoศศ"Y”หคฆฃYVว_Ž'lุโชย†T—f ล2 p‚b แdŽ€ณxN๔?_ ผGpภ๙Axบ1๗/ซ#คคh> ตฎ+‰Lื8ฤัแงปฺธฎธซ๑x†X•๏XUk€า9ร>I ‰h“€พวบHเ|ˆิ›2ล_‹U๑าศ?๙Zฌ ‘๒P-decำLลบจ๋&แบz{๙` }* Hy”๘\˜ผhํฟ]ุหy็พ ซ# )JK๋zฬ๙OŽ,ผLฤดM ิ„€ะ๑)ฟ%+jๅR,Qปด๗วาใ๕]ํ>\—๎๊๎ํซqๅฺฏ_๕ž-ฤญ0อฃq4-vง’๕ษํ๊cฤ˜e‹]1 ถฟD๘€HzB฿—O*พุ๋ตk —In/สว4๖Dฌ‹+วtYํ<ฅ|?fbยf iLูQษG๔€ฮ!:ื~ใร{@ณ/%Si$Nี๔๑ฤผgƒ,|{Hัฅ2าๆKค/}ศ5bXŒฒ๒xs_๙Wฅ๛x[๙๛ฝ๓วqณc‹ฮ0๐!L8Xศ~?`ๆ #„rฆ’qHห‡ฑ/HงSR…ฉƒ'ธฤW๏SZด“›๘ฉฏk5ิ6…uY@ย—ิ•:Di์v๘j~ฝูŸvaฒ/hx๔ฃ`เภ{€ฆi…๏ศว€ฟฟคด‚{gพdเ๋ELSi<=EใObซ๖็ฺภตB})ฏ_๘ใmๅ>ึ๎๎ก๑ปƒ๗็gฦl6ฦุูข@คŠ}C]ฉŠ๔ษ4} .ิาYฃGjจ"ˆึ€ถจฆŸภI๘าฏa๚D9Y%`‰ฃˆทfp#ฮญ:ุ•ต|€™ึ?tQ@ูo๋1€กื7ฌ฿‘0O I]E YxHฅM๗คฏMชMี๒Cฤฏๅi J๙xะฉ’๕d,๊aปo๐๚พ๖Ww•ปบkCใ/ฯ)ŠยฎV ฃœG@ฑจw'$ซคDผง๚ๆyštฅzIDก@จ๙~? คi}ƒ@‰ณิ2งL๛ ิ{Š;็)…›”:‚ฦLฏGฉkขๅว(šก@ั๚ล˜มะ8ภบAlš6—„ˆ;Uน‘,จใE! /1! ฆKชž๔ i฿8ึดx‰ไ5แ`ฉฒH rq_zธ฿5๚ฎvŸn+wuWป7ฏ s~fอูY`B,m "hฃOƒ';K๎!€๏Cย๙1๓พ&~JiZ฿–๔ฺAA!dxค ถฆ?ๆ๘dอำ‰‘@q:ดิจ ่ย|ีพ _ฺญ๗ฮ!ึ B zฉใคwH๔d1—@่• •[BRz์ห€1กดภF๕ละ8„‡๓\•๎ทG๗?<๓๛ฃวฆAึ "˜‚TEตNCชG„๑{๐,>@m‚^ DดKO˜Hห๐ํ$๊ฟ๏ ฟถ?3eงธˆภฟH/%ํKe&e%‹สuะ๋ฮ„V^ื’)‰Xr*>๒Sธ/ู๗ว8ฆ‹f~ั" ๆukx?2๚่็ฺeํฑช“,าป-๖ž“ฮำ๊ัฮตๅY#[พnคHง!@~๐C‚@jL€& ฤฉ/R{g๕qwl๐ใm้ฟ*งปฺm๗ฮ—ขk๚•หX0NZ@U vฉ่ฟ}Šœ&ๅงบธ™>`Yทผฌ ”‘ฺ{ยQOา ง *็`bู`zjฝ ฦ"๕T๊Jษ'๛tqžแฆ„N5๘dK@เ็ัtัƒ๙ซฑฌผ/kD็&๋&/แุ{.%-”q+ ~6d ภหฤํWPOzฝ๐c‰9้/๙๑zค)ด นnณ>๛ฃรOw•๛๊hผ)ํCใv{Wฌ ƒ›3cฺEธ@ด๊ม @ฏJ4niั ๕๖ฅ๋๊๐6–,ฑ-ฏSฺ๖E{‹@hhนE๗1"๙“zP8a๑ส“–xŽ48`%ี;๕oศc>๐-LE!ภ8h<`ํสฺ๛cๅฑฌฝo|๛M #CG(ฆLœ๒ใw@R๏เ%€,ผT„่%๔–‚Kt^>ๆ็ง?<ญ+`‘ช‰ˆPึoj๑ถto*wu[๛ฟslcอjU@Q€™v—š-ิO84Uˆนูฯ\tษ–?Rพถฯ ˆ‹ไ$;Hจ)ใ7ด3๕]หฦuF฿฿T€Lh7oSหฮจ)฿1LฆฺM้ย’ฎš1!  nห๑Pz_V์Dldดผง๘๑๋HBDฮ๙K]/)K9R๚ุb>ฉ‚f ญpโ?S?เ๖ก]่รuป8ะ๎เฝsฒ™^\N<†QอInU"ิHxอŸฒ•ส๗ƒXDฎ‹ไœษXั}Z' HไmX0‹ %ื?{-+}›ˆฏs^vVG ž!ฯ่e้๘๖๙ภฯฦ:(ฦฯ3‹ง๛„˜A8Ž‘๙คž~ฅ?J๖œั€๓ภcๅก๔X9฿ธ'๙@ชโ๓X@บ^Jปพ(ฒเๅBRห4๕Lr„hzleพขื„M๛—ฮณฌm’๚ฐ๎์ช๖ธ;8t[๛฿>–อ๏7๖๛ทk๛uกฯ;ะ/ฤ?4S ู-ZฺŸ?ฉ4แ–K๊^(ˆmฉE T†]๙xฑ ฐ|šฎY จyไ2แฮสJ'ฒเHu•๑ฺ%™ํ#ฎฮ^“โผ,kฐ,๏'kG@hšœ๎ฃฐ? DฒOำ”Ÿ๏…!อ€๗ภ9จฤฒB—ฮ; @ยe R„€ุe‘‘]ฌœ<2โ๔4d ภหF๊ฃ=ˆKฅแ˜i>dฑBซrม EฺN—cๅ๐๚ฎrฟ~<บ฿?–๎๖ก๑U…Nป"Sิ‘%ๅPiขชฦคฆฆZ็˜@๎ƒZธeAฒ@pห€" $฿uA8@šวแพ›้Vบ'ณ|๊”B‘Fค๋ ว’žš3ญุc‹hขK๘Žบ™D๖๑—Œl๗ะO๓วส๛c๋๐ฮฉK฿ƒt1๏=ˆค/ม'€l๘@Uท%็ะมฆ yง‡„”vqhึuƒx๛ะ๘฿>›ฟ;ณ๛ฎ^ํฟบDณcภ0–ิ6Œำลฅ‚ปrมฅ‚๙Hj iณGต3จ๑Cโ98ŸชŽ‘€ณแ”HไคฎH•จeJพ{:~š‘๓f7"PG—Gใ5๚v๒išj๛"๔ฺX๒๚๔sดM”aษ๒oŸMา&Cฌผ@๓4ำฅ›€M็๛?V•วcๅฐj‚H *วกw(้K฿กv<[dเๅ#ลfL๗้๖ฉˆ?ถFšภJ& ฮ#๏kใชtฟพ?บ7ํส€o^ญฌ1Vkbซง/ํ>ฟpC าธ6t ีWˆŒงjz^ŒะSหJ 5ำSO‘K5L IะภHf๕)ค)XIฒ…2กlˆ"‚tธ`*ต‰ า๏O"’{ฏฅใ>๚๔bะeHไ? ํ฿ ย€๗upฌะ๏K๏ฅ๓eํฑ] ฅมžษ/ #+?ฝ๙yาq*Ra! _/bo๐ฒืาSใ3 ฺ5ฅท0๏๋ #๎ฮบซเรu้ผฉ๕]ํพ^UalQX3XfญนK›ฤ๔๙Fพ Ÿก‡ค‘SสjCห5Mล  ฝ๎T๒Ÿ ถR‡เ3ฮCแลฯ"แ:™šŸ?TCญ สธ๐d~]รฏAว#@ฺ0fท"3๖฿ˆช%`ๆว|กJ๘cL@ป๏<@U#Ž๏m`Y{lœŸF•)Šiฑ๗$”_"ฅฤ฿# / ! ฝก๙›Mห‘นไ‡ๆ๖‡~tŠ f-ฐ6€ะอพ ํK้X9ผึ๘้ฎrฎJ๗วuๅฝY‹3kฮฯ-L–qป„ศ—8ฝ2ต„nkาซ!Dโาํ สrด(4๐.‘|]คjqJdฟห5qสฒข–ฯฏ ย๙์Zb{ูyTPd5A&๐ฬp™ษฮะD4ฤ ะ 86a& ๔๙œก'xั ภ i xPี่๗G๏w็ฅ๗Uƒ‹8Sy้ฝ#๔ษวŽŸ5ฒ๐๒ำ%Oี5ฅ‡๘)~)>ิ_/๔cT˜กํ Iม#ภแ่ํCํธ>บ_?›๏ฎํป7+๛๖อสฝกธOŸje–6”ewbhฅ 8L๊ŠuaึฒUM%๘หdลหม4Ÿ6“ณฯห*]ž•‘4lๅzณ4m๔O$ ‚ฦ„^hŸรฏร๋โZ?ทvฤc}Hส";ฅ'๎1Ÿน๎์๓แืมtฅฟฉฦ?ฮ,Xึˆปฃ๓{๏๖ฅ๓uใ{๓ŒX9A‡Dฐ%๏ฎ%‚$^[}:พ$๒,€—‰ิG๚‡เ้ฑŒœ—2ฑฟ%x‹ฐvถ‡ผฎฏšWฅ{ุ9ฌ๋nFภl๎5Uศห™hgขvซ4S%ตุ]—#์›ฤ}! Ce๙u…ฒ*๙ศ?ฅ๏โkUณD˜Hšฐ?‹๘Œ“๘q"3ฤ๚I&3 ศ๋ฐm_ืไ™4B๚(ฟŽš<์๏๒ปอ๏—ลF๓ำŸ็š=ˆš?%*ด๋hœc…~{๐n{hก๔Xห ล๏C๏ฌ%[‡”๋„ฺ๔l€o’ิ‰‘๔ะž๒Oฃญ๙ฟD`ˆI฿ผZฟU+‡Ÿ๎J๓‡ฝ๛๕ใกนพฏแ่ั5ุ แฯF‘ฆQมY9ึผYบFZก($ฃ๎๓sbG้ZŒภด2กถฃิ&%Ÿ๏‹ำ๏X:je๛{ย๖'๙ัฮสBvƒคก๘ุIฤฏฅk‚$ๆK๕Bไษ9‚0ภสข& >1(?Ÿ๊ืึ฿>ู๏ห ŽG!ภsหภ@qOญ๙ฟช๗G๏๏๗88ืYNีฒCฺป๔ร„๔S&{–ย@พHjW(‚I#J ?์!ฉ™ณฤ„: ภAK๐…Pฦ‚HษRขกœ๓ˆปฃƒ๋‡สธฉoŸJ๗๛งาฝบ(์๙™ตgฦˆZๆ0Š์%ผว#ไ/ZB๙œ$Nฑ๐ƒุ ใ €•๚9ดบFข“๋eนบ่OˆdCใ˜R/L๓‚๕าf๗U8‡๎ BภD  <:ี”ลi4ุฏ#๛>_Xๅo๐๙๗Aฤเ<@]ทๆั๛๛]3X:@ˆ\ฅ'F#า"wํ†J;TQ๗9! _bomzLทKฺ”<ฎ™/–ฮะฺงูฑี1๓มเv฿๘ซปส๖๑ุ๚รฑxfm฿ฟYื—`‹BจYU&&๔‚ฬ…‡bไส9T™xู(น๗ฤด ๑ย๐วฌw,+ ๕๏ฃRฏฆY cƒาฃฤ๋ Œ‡่7‚๗Eห'mฤ.oะโ‰๖Nำ'B๊็๕Wœฏ0ž'[˜ ธฯ$ฯh3|๙owpแะ๘ํกqeํ‚๙’2€B^ ัkdฏล@`๛l‘ƒ_&bฤB่แิibA}1‰๙ิ๕–ฮ ๚ฅi3๔ฎ€฿?ฯ์๛~˜เย_wใ#ูฝ๛LMร} fušq;ภc)ฝ๛)?vพr 1nA๚>.šn'raต๓H{ฤq=๖ๅ]oถ๎?mฃ‰—EP๒šO#๖ฅภyDฟ™i้ผžฌ’6=ๅฯ…3–ฃ4(วภบ<”่w๏ฮoฮํK‡uใQY(๔?ฯห,!๛TA!๔฿j๋ณBถ|[เjF า?ื).˜ ฐdq ฉ.๊เ“šบvีธ:บ๗ฏือฟ^ฏฟ\=๚UึฎŒฑ3^S43พˆPrL€tื”Bจ%pญ]^žิ1X2hฟX'ฺญาQuiษสูH`ํใ๙Tsf™ข‰^ั๔‘๗๙ฺยC’ษ?ฉฌ0ฮ˜Vv27Ÿด™š๘G฿๔<์,jู‰†L๛็๋tDฝ฿'Sผ‡แ@c@๏hฟท;zwp๎แเุ๘ฒ0$YBคœB!%%•๔ฅ๗ๆณ$Yx™Hก๘‰ฟb1ฐ"y ๚?ุ)„›@ษŸท'ฤtมฑ;V?•อว•๛ํำั]V๎ว๏ฮŠีส˜scภZkd7นฌa,Z‰ !bฯ_+š้Ÿ7B!~.™๔ AŒ4aZJCฯฏค,ฬŸB-mุ—๊๏“„ถฉ$ฏ 5ยX!สคธ ภŽ'ใ+ [„ธS๛8œ]~ท?Yิg/0ฮ๋"ณ&@๏'๎„!บ๒รT@jAf8XVˆฃ๗wญ๏฿ต+z๔ˆ๙๗ะH8๕ฅ‘|๊T@Ixไ€—ŒTQ+ห฿าง4<%่%ำต@š็#ื–$๓ค่6D€ฒ๖pทญ‡›า้่~๛tt?ผ?sgkVซUฑZsอ.4H๘๚ิ Aฤ่+$คiย?†W”v`$แ@ํwhธQx5jึ œW;์ศ–ึ‹<1์วำ&Lฯ\Rฺช–•cซ1ุKZ='๙‰ฆคMผ ˆ<๚6ฐ?‡ HH๚ ภi €sวพqทฆน?4๎X;t~g”‡-Dย<]#๗@›าช[jƒด ้ŸYxูะฦ!UO*’`cไฯƒธv~J`o Y๘l€G๛’j๒็qwDธ~จืG๗หŸวๆ‡๗g๖๕ๅสพบ\ูs์l๛ขˆมศuย!ไEฏญ tk'$็†ˆึ!ิซพŽ4A 2ซ„_wณ)qBW†บbeQ)ซ‘ฟิ;9! ั&zฺ7ฎ้๗–*Ž็‘ˆAภhซ๒@‰Ÿนศฏ7๛๗qž,ำึฎ ป฿5อ๕ถv๛ฆื!!R•Yฺ~MHXoฌLจฑผฯŽ,|;jฟไ๘ิคuNQํ?u6ภ๔Ÿ+I@hโ๎ะเวา๒็ก๙แฆ๘ํฦ}nSผบ@ฐ=ำ 9๚ขฅ>tร๒‡๗;ฒ–œโเ˜ๅ%™ป 0P6H!s?'Xvฤ>™โyฑz%b%ฅ}ฃคœ'B:(๕ร\ฐ™‹\ˆ˜ๆใdฟอCR๏hึ๏ำแDc๏ถŽอ@š?™๖7Z ำ~/< ›ˆ0 œ๘{อ฿๗s๗ฅ๗ทปฦl๋f{l\ %^Ž%ค/i)ฺฟ”{ชOช—,|ฝะโ’\า5_ปfาN4๒๗ค]าย@QUใ๑ๆก๖๚s๏ฟ^7?~^ทฮห•=cภ˜!ะOธฤ0ฺ8Mฃ„2ใบฎนšMgZ‘-๐N‹ํG๒{าุCณ<Lใ@MNศXจ‹—˜^wRฏฆษKuัk๗๙J฿yŒƒjI mไใ*Œ m๓`'๛ ~|ฑLO๚@สRฒ?๊ำ—mI|ผ๖,ุ“wO๔”๔Gธ0ภฤ @อhภ{ ฮตัว qw๐n[ป‡สํoœ๛ค๗Vœ๒‘,ฉฤ*™ๅB@พnH~Vิ‰ด=ค๙kiœะ้โ>าย?)Aภ…mL‚ย๗๛ฺ๚๑เ\ฎš๙oซ๏—๋ต7ึุ๕Œฑ@ช0Œ๔อœ3`2C ึB้ฎ-–gN ๖XY‘ w๒|ึั™ "„พaคฎY|€ ‰ฮ๛+Œษlmษj@ฏอฝูุLœ์O]ุ]gŒว#B@ฏ๑ร่"Mt&็ย@มOฺƒจ˜ฆVข๑SG๒๗๙{4ะ8ƒe ธ/;ฎqทปฺํK็ิา๘๙q๊{,fHuฤฌ)ํห€o)„ุ/q2ึศ?๔ฯ–J๘š*k‚ษ๘x๘tWบ฿>ญอoอืe๓๎๕ฺ…1Eฑ;™๋วkcฺ|Œc๙ํแwe8Aธ{าพุๅูE•ิฒค‰•vใ๔ิูЁผ?|๊ป๊'}P?ืซ๚p]Aš ‚„q่ญ>”๘‡}2N„เ%8๕•X@ฑฉ|ฌ,vRม8otA หYPณ7Dฝ€ฏ 0ฌ฿™=[ฏq€๛า๛‡ฝs๗{็๎vตjฌœ_ธ’w„4ฅBิ>%ศภืฎoฦสJo๖šK ืยน@pสTภ…€๗ฎ:ผ1๗ภHวˆPี๏vตx{๔ฟ~:4๚phพZ๓ณย\œฏŠZงสฑa—d~ืx^T0แ:8น๐`<]a„) ศ๚งธ!0’>fณXึฐื๏Tฃปธ “„˜็ I‚›Ašฆ‰ำsgšdล?ชๅ+‚ ๖ƒžภ5ษงV7@—๛?๋อc`๏ ฐ€hม{Uํa{๐vธ›mํ฿—ฮWว„นแฦด|‰ฬ—*ฉ‚ภ‹@พNhฺพTŽ๎?ๆGIX2ั๓™KงRaภฒ๋—'ฆ}L @0ˆeƒทฺ้่~๚c฿ผ{ฝถ๏฿lŠ๗o6๖lำฉ๘F๘Ÿ7Fพ2ท…ขญ $M๒C๐‹„ศ=Vถซ—jหณWณข}Oง—Kส็eQOใ๎5_ฒ`ะถเผ.Fเ๓|ล $Œ’ฑŸTก ำฒHสฮ—6ำ| ~นcSฉ[@๘ง๛cด_–ก‡ษวสรน๋‡บนูึn{l\ูธิ่กปยร•"ฤA!า  ด‡ทํู /ษคvBฝม๗?N๚ฺ?ท,๙' iš €Z'h฿—08ธ?6๘็ํั๔วฎyfmํ๛๓โว๗gล๙Yซตi—j่†Gณฏp0%Ÿฅ;#ํซe๓xะ์฿ฅsอ#C8k‹`๒๒1ะทHŒ„YZhD!a๊sŸ_ำใ0ต6 p}^†I73M^ ค๛ำ๓๚GŸ=-;N๒C›ำ0ื๘น% ทะฯz[๓ฑ๔xทkšOUsณญš}้|ใFgƒ๐”„4ํะ๛็Tm?5&€ท”ถ=;ฏ"ใม๛Fศฃe +# 1อฒ-ฯณl?ตผ”n#ืr,yผf๛ึv๑เl]˜ฟฝ9+พปฑ›•-ฌkiK๐a ำiFูBฃผjH=|๚Pท‘Ÿร!ตลbSส"-ง=bZY›b๙b?@ฺWอช0ษ7๓|ตฌ–b]Hฎ0อวฎฝHหภhฆ7cUัkษžเว๓๛kะAC๚๒#‚]฿!n๊Ÿ๓Žเอถiuuฌธ/ธ=67ส+็˜’ šฆ฿k๑Ž๖ซษV๚5ย๙๔Xs๐ถ=[d ภืIฆy=๚2กŸdเ๙4*_า%I\š๊งาE‚คุฉ}กG-#๓pฟฏ๑๗O๗›M๓งC๓ฟn^ญ^]ฌฌ-6ึฦV3sณัY„ึฅฺt’ ™พ้E๙~ฬาkณร–ไ#ษŸœ'ี 0ณˆSๆ@(ณ4Ÿบ"็๓๓T^ำQ ŸZC„kฯฮ—์อ๖ฝ–C]4zิ่้9c]Tซ`fฎฎ‰ูฟป'ฃVOอ๔๓ั5ๅฉ ุ}๖ทjง๙‡ƒ๓7บนz(›ป}ๅชฦ๛q๕‚  ฿Jฺ74 žš๙%"O9?fฉxศ€ฏ &p,ฉภา–j<_าฤ5M^ณhวš!tฎิ'i|4+มฐ๋<‚G„ณuaพZ7—]0เูส\œญŒ]๕Ÿ R4eชๅ๓ํ$\^ณ oj5.@“&"Jร‘d4๊ิส๒๖ฒณืซขฉฯ ๅ„8ฉKัะgy†ฅ๑:ฅฒBฟฉึNศxขต๗E{ห€f-ภัj0บ๚:™ๆฯ, ฝฅ`ะ๘‰ต`ข๑ูŸธฆ~u?บ๏ัv‹ุ๖ซ๚ปฝ๓–๕OwๅฯŸ๖๕อถjŽตC๏ฃ.พ )ถY๛งn  e๐ น€ํ?[d ภื ฎKR•OJCvNฬ"ภ-Rะ_Š-d q’U€Ÿหืะ^(šU`6.ผ\W_๖อ฿฿m๋ื+๓๎๕ฦพ{ฝiฟต%ฐซฮ”ๆN‹า’ sbฎ2)‚ค-K–‚ษTฟPูXฝยQ— ฆืแ5หฮ’..๏+ ฯื"๕%ซeg€ำฒH๎฿Ÿ๛๛Kาฉี€jยšรXŒšฐOƒ@ณŒZ>๗๛วœƒะz;|๖ww๔ถ]๖ทนV๎~_น}ู.ฃ0ฆFฆาป%dH ๘๓‘๚b๏ยูฯู๐u!ฆ๊i๊˜‰”‹iš6/๙้cฺฟ~š%!C ตS๊'฿วิ{„ยXฏ ณ^Y๓๊bmพ{}Vผyตฑ›•5` ะ๏ึ4ฎํฯด~้<~{#ต$`$ัต”กื๛aิแŽh่ภ^น‘๓P้8Qา–ข‘หm™[n”ๅš~_V๖๕ก,ต0 B/4`_Ž๚™f฿๏ศZ~Ÿ๎วผม๏?ั๘;ญฟŸ๒ืgแP!์๗วmูz}จ~๙ดฏ>š}ี๔‹ Jw™#U๓๙นฆฯญœ€ R{Ÿ%ฒ๐๕!Db๑Ke๘[["YาRศyฉ ๅง  Kc!ผษ1€Œหณ•๙๎ํ™}๛jSlึ…) …5Œ%Wำ„€ฎjั 4ูHM…่%"ฟ‰y\’ ฅฎ‡†‹ทฯฐบI;ค€>^?ฒtัœOฏฃ๔'่B˜“r8p•‘>ฟRž™;5๋O„ู9”๘Gs?ŒBต‰  ม}ขูŸํ1ํำ}๊ ฤOำ4{ ฮ[จํัป?๏สๆ—๋C๕ห๕กใ๖P฿ํ+ืEว YCไOn๖ื\’ห Dฯช๕ๅ/Gv|}ฬKฮฃคKf๗ภาRข?Zะ-ณ ฝn<—~]X๗รปณๆ๛ธw?~w๎ฮ6ึลฦฎ N œiภคv้๖ะ+ใT๙u‘Bาํ.‚R}ผ|ŸVH'^—™ํฅ๓hฝจคOฺ+]G่ฯฤ ฯฯํ“ฮV5d๕Q3ขkaฒ4/น6“ฆM‚‡&QX็๔C?ไfmง๙ ๊๋o!);ฌ๎็e7ภ@๚ญูฟ?๚ƒ7ปบ๙๓ฎl>›]ู๘f๊๗—ภอ๋ฑ๗fนbไžB๚า‡๒ฟฒเ๋Eศ–ซฉ}&P.ี j๖Y buฤ,)ื ‡ฆ๚b;+ภ#เลYa^ฎํซ๓ตน8+์ๅ๙ฺžo c- dUN4.}ศš4ำฎy}ใH~ิ ฉ ชHzTร5#~’?1ฅ‰›ทMt-๙Z$-^Xฤ~h๗aฬCbe$Jสำถ*~ฮhy˜, L4๗้T@ชออฃ๚^Nญfj5@PตQ๓ท$ฝ5๛#ZpฮBี๎Ž่ฏ๊ๆ_Ÿ๖ีOที‡ปC}ฟฏ]p w[ณ8ถOงํ…ฆi–j H5KOจ๔ด>dเ๋EL i&P.Dž)คžJ่ฑrูงึ‰}ะฦhrฟฌ6ซยlึึ…5—็k๓๎ีฆxuฑถซยCุะJ~ู'†%a ฉ™œ`“oaจๅQแ>๒๑“sืCฏ'ีห3-+ญ] Z= Zคฟผ>^nZ‡น฿›F๙+B€d๎g็ฮข๛ajึB๎T ืฆc:`ฆk๛๓๙รฏ#~โฏม}‰xปo‡๛cำง]๕ำว‡๊ำฑ9t๋#9vน9]ำ๐ฅh}mฟD๖งšตO๊วณDพ^ค?/าŽ—jึK„€ุtพ0 Lฑ๐1๒fiถ ฐึภ๋หต๓โ๕e๋ฐŒ1F9„9๑ ecทCอ`B~ฺใ!B‘Hฝ ฤ/ณj0๚ฃ„ฉ.^vV‡ฯฺ‹d|ๆ>i๚ิฏ?%๒PYิG๗iแTเ0`: PY๘G๘๙Ž๘๛น}` Xีw็>>Tอ๏7ว๚็O๊—๋]}ป/]ฒฟฆ=ฯž˜ šIิล กe‚ลง2๔ฤ>dเ๋…‰์k๊˜”wŠ%` ฉง 1 ภ’ภร€Z+Žษd{ำจร๖๚bm฿ฟ9ณฏฮWvณ*์บฐPฌฌ™ฯๅ'ีmŸ \@Pnฉฤ6คม<_Œ`ๅ'ฆ๚!@ZN–œคล@;ฅฝาซT"ใูk™ŽS€ไUKo3ณ™`.pอ4ี๛๔ฆิ/ŒM@๋ŸนคดY `ฏม“ด™fฯงqณ?uฐi~ผฌเ'์๐ื๙{ฟ๘g๐Pกฟ5๎—›}๕_ส_ฏwีอฎtวบ๕Gภ —’~ˆ่C๓c 2ฟศี(ฒ๐u#dฺ็๙†šไ‡ˆ4& ฤL๓6q›*,Z๘8E€+kLa-ฌ‹ยผนXwฏฯŠ๓๖cAฦฦุiี@^๊W!|uQ H3SษHWฬ็’‘ฒกน๔ม๋& šน_ซ+Jๆพฎ(ลห"ฦK๐€@J๖Bพฐฅn‚ฉูŸš๖๛v$?q€D๚ž>{ณ€4ภกBผื๎รฑ๙๙j[๔๑ก๚pทoถeๅ็Q}"%"ๅZ?‚<…o‰้?%`‰๙Ÿท๗ู# _?Ld_า†y>/—B๚4๏”ล€N"ดrK้€<.|ม€1€ฮyฐฦภ›ห}๚ฌธ8[™UQ˜ยZc ๅ2b๔?€:+€Zfฺพ&ฯˆ_า›ฅ eƒืฺ63ว?พ๒C–€™ป€๔aฉ ฝฦล™&XIู้๛:ส";j๕ภถำtช๙ำ >‰(ัwmB˜?(&0ต ุaกลh๐*gเแะธ?๏หๆื›}๕๓ีC๕หอถพฺ]Y;D@ํNฬž\แšวฯง…~„gHฤ฿Kไr๏6_#`F0ฯŸ•eื•…iฝผ?1’W๓ำ4ž‹™[b>…o0๋ำYูฑ}tŸธจภœ;‹ษ<เฯ šd'y่,ฬ}ฃถ?พ้V๚+:a ๏-4ฮ@ู>pจ๙้ก๚้๊ก๚ใ~_฿ํ+฿I•ๆG—ศŸ ฑ/q๒ฏ„ข้Sa€›๙gยื๔Z~ฟฯ๘๑hปผ้ฝ„V๛ฏภm้๕ฎj~ฝูี๘xW|P]o๎ุ4)ณ;S๒_:็_ำ+ถ•|ก)ๆgMY๘V"ฎ-8”ŸJi๓O4๒Y าPาิฑE€vz”Xฏ x๗zc฿^žูM็XYkฦ€@แ๒Fบ$€ค๑&(d<์Cx„tN CGP‡"„ค๘ะฅบฤ๐ช€vา#iRˆ]Nfc:27ำ}šำ qะ พ๖2fVvˆ ‘dž`เ&~03!ภซ?ศb?SำP;๛ส๛๋]ๅธ;ิบV?}บ/ธีcๅ?[๑Wธ๖%@๛๊_hฑษำต@พgI๒!dเม)ฺmŸž"$hiRR3ฝFฑo,,ฑ6&Rž8ฎˆGhœ‡ข0๐๊|m.ฯVfณ.ฬ๙feฮ6+c‡x€๎ti‚4[`&(hท@๗ํC€$ป:ฤsuฯ$2า$AๆdLหชdฎี๓XE!`–ฟ<ิWปƒWต๘'Fyฐใะ—–ฎ๚ }๘วฑ6 าf/๖??dเBชภฯ1%AHK !šภ$๓ญิiซ•ำสจGcŒ)ฌย8_ๆ๕ล™ฝ<_ua5ฆlฐุํ>I#zอ ”ๅ{ษ๏ญjฦ,ค๔€•arฬˆดฯำๆ๘Ke๙6T6&hš>,ฉ+ฌำz%M2]P†้z0j๑ฃv฿ื?๛ly_ฆƒ(Lƒๆš?๛๙ึ เ;อื™ฟUt_tuW}๎›mYนฺน–ฯำ%อŸj<2Ÿj๊’๖"~Zžึฃ™นPB๛ต€g‰,|[H๚ญYP&Dกฒฉๆ๘”๓Rา4MJคc0`ภy„ฦ!ฎW…ysฑฑ—็ํŒ€uQ˜U์[R(แ๓sižั.=ชi›m๛6iB@ฟฌl่•จนBZคอ‘ฒฉ& €๓2-฿Œู8Jฤ>7๙›i>ห“๘cž!y0DOVœฤP+#|?Z<ศWF ่Ž pยกFผ•๎๗ป}๕๓๕}๕ำี]๕ํC}ป?บส9 L๛FT$ำฟ‡๐‡~ดจบ/๙C‹คš_๙ไodLะพe๙lX^ฟ]๒ใ฿04O๒๚๚žฤ้7่šฺท ;ฆ๕I?๚ฯอว‚๖_ปeํเำั!ผพุ4?พฟฌ฿^žูuQ˜Uaaฝ* ป‚ถYใ†B#I3me฿๖Cw{ฒํบ—bŸิo˜ํ[นŽt๎@์=ณEฺ"Q@นLtKn๕d‚Zศฏ5ษ๏ื๎๏5๒ฑฮ๑{ำํจใp8ตฮ @2วŸ’<:nไœแืฆyฒ?ฎGื๚])ปา๙ซmูzปญ~ฝV>์๊ปรั๊Z๛ุ๖”ฤศ_sHำนu@"wi”๏h‹ิฏgl๘ถาZcV€ชj"็kๆ-ส,ัCeค๖Kc2iˆตC๐aUXณYฐ*,œญ syพฑ็๋vฉเvึ า]iสเฤ ปl๒๊าC ใ v*nู*ฌ75dI•U๓ˆ€‘d) V•@;ฆM-ำ1™šํ๛sฉ้ผ+~ค B๔?ฮFฟภ~ฎ๙๗ฟ-kฤmูธOc๓๓อ}๙_Wwๅฏ7๗๕วํพูKmมŸิ;/๙c_‹…ดSพ๚—J๖ฯV(ศภทIK1oวˆ>”&ๅq’ถ‘ฒดฬฉหว\)Iำแ฿฿yฮ#œญWๆํซณโ๒leWEaฌตฦZ;’= ีชD/์SKิDQ‡9แ๕i"! ๆzNฎ3จไN๖5JคqW†”ฦฯ3ถQำf†jส'.$ใ7+G:—Ÿ๖ 'eต5จ@7๙#ะ๙Zคธย_๘WB+Tฮเถlงัvปญ๋๚ถฏO7ๅ๏z[–พq^bMK–l;’ๆš๓ฏ๙+ะใBมT mY"<[โ๏‘€o &’ฎi๗ฉๅRˆ”งŸบ`=1ห ร‚vkใฑด‹UC็ฮ7+๛๖bc.6+ณZu๑…m=๚3๒gM1$ภ๒jLศ$j”~LำฆyA ย ฺ(/fˆ•“ด~ญผR’s†,ๆืฬํdFโŸฏๅ?``๙^M๗ฃ0@หNgฺQHYใ—ๆ๘๛ม๗฿kํtฟฺ[ุืฏ๗ฅ๛n[๋๖พ๚้๊ถ๚ๅ๖พพ๏]ี Aฺ๛Fป{š้_ EKhุ็‚c <{ขื€o!ฒŠปVT๒?ล\ส/v]ร‚4qœZ฿jำx๐ะ๙Maญ9[ฏฬๅY๛อkฌQอ'’ฟv+ษฑFถL๘ฝCzญGา๋&e%มdKJ๊&‰ฐ็ํฎ-ถีภœ iw๕Rb…ุGm}ฺž™ ~:H๋ํ‰:๖ฃว~F๘Rฤ?๚/:A ่ จมCํ๑๖Pป฿๎ถ๕?ฏnซŸฎoซ฿๎Z๒?ึ5ต‡ฤ,zฌBQฑOฦ,!€d๗6ฟXdเEŠf/ณฤ "~ผdeภTแ!ึถ%ํ–๚‘]•?_฿VWป]sฌk฿xปใฮ„ศ?ี๔าน@"เซ |Ž,|ป0‘}ดSส-!€๘ื๘y6pอS…ฉ>น/ก1ฅlKฎœ๓`ภภลฺูพ:฿˜๕jeVึšขฐPุฮ ]ZkA์๒ฆZwฬ™ชUงœซz|1RฬฯืสJdฎ๖ม„๛คPึt*Ÿฎ‹,ŸฏโฮฎญเฤB€“๖๔็ำ๒sŸ<เOา๘ว<ำ}g๚/ฤ๛cํš฿nชT?]_Wถอฎ,ฝฐิ/ศ#)> œย)พm€๙งฮ๛*,|ปQIชy;deำะ?—@ปฮาฏ‚าุ˜B+ิฮ"เช(`Uฆ(,œmVๆโlmืEๅ@Cฺ0K๒™ั[ -๊360m;q~ƒ\งVV$iรดsde|ฎึฦT๋oท(PL๗'„•ไถะฯpN/๋๙ิะ—กัฺ๑dIณŸธบ8OศธษŸอ๓‡xh?๏[9ภmี๘C๓ำ๕m๕_W7ๅฟnnช๕ร๑่k็ฝฒิฏ๖tI„๏…}:}“’ๅ~—Iไ/Ybb๐ณGพ]ค{ŒไR ~ia Nๆด ,ฌ?&ะซ38ฮฺูUฮ่_™…ตๆี๙ฦผ:฿ุ๕ช0ไภ้ชภัแ"MZาghฏ/`้๊รe'ฏAV6DาฑผSศ?ฅOj\ ~๚ฉK@ึ๎ง์้ฯaฤ?)K!am)˜O4mŸ‘~/๔ @1 ะฮh—๙uf_6ฟ>ิ๙้๊๘ซซ๒๛‡ๆก<๚ฒiRศŸ?)ๆiตฟ้?Fกค…ฑชg‰,|ป0‘๔ูŸ—“ฮ ‘จI,ŸJสงh๔’‘๋HใPฟ๕4็[K€ว๖หึZsพYู๓อฺฌฌ…•- ฐ…ฑฦฮซ˜ &ู>3ฆง„ผถขู…๓PmFษ_†cมฏ•ๅGJคป$ vNๆ4]*;9 Lž่‰ึ?[W!^ำo๏ณNใ๚f0๗ทnƒัเมBํ ๎ชฦ_ํอo๗๕ฯืที]_Wฟีท‡ƒ+›ฺ;ค†๚$’ฯI_๒Kšlษ฿ุิฟุาฟšึ/๕ํE‘>E^ ๐EะšH#œC๗C:XHย๏LZ Pสฃ+zแ˜ฎจmตฝฆดJ` ำ—AŸื>nณqฎ‡Ÿ๎๖4ึsพY™ยZpืฎึลสดณปnย(ี๒Vฤ๎พ–žL”8OŸฟIๆๅ&ํb าЁโkฺ่u+dฏตYyชcwN๛™qํำฒุ•.IVํฦ•ฎ๔g๔๑ฺAˆกlช ๐๓% Rฺภม –รปcๅ~ปจ๑้บ็๕u๕แกน?ฑฉ}็๖O#ฅใ๖ฏMิ,’O‰ฏ!NฺS–๚_๗์€ ‘œ„|I0lงKdOทS‚]"(pย—^d Eะ„6ฺg>าxฮาGุ*฿xึšๆ|ณ‚ยZฐ`แlฝnฟ k[ฦEฟ\0{!tiฐ5ดX๛็๙(—›ฒV … ƒ4ย๊ ผ 0h}ceyเdKฮiy›<๔VB๘ly฿ด‡๋เ,๚ŸM–๏“๏Lศžถ’~l B็หฦแถฌีะz{_ื๕u๙หอm}ต฿5‡บF็}*๙‹w\xj$ํ?๖ๅ?‰ุ%! d๊ญ๖ง๕S๊ใ‹Bvd่&์˜ฯ;TN3ก‡๒๋เ&ท@๊ตตvkc)วuำN๖ˆเ=@a lVkำญซยยŠNšBชDญi bVm?$$ˆว„x'‚€™– ]CJฅ๒B–IDAT]‘ƒ,dHะH‹3 ๘–๎Kไฯ_’`…4^ึ ็Hi\นbV€žฅ๚“Uใ๐v{ฤฆ๑PX ›ี ,(Lป_Xปบ<_ูี  (ฌ™ปRว#y๏<฿Oูฆ’ฟชอ๊า๊ ฝ~ฃš?Oำฝฯž ิฤOตึง?~ฏ?ลŒ„>| ;ญ‡Z!`rMพฒ!"˜ูl_gู่HŸ xhTแฎ.ี~rw[rw[n›‡๒่*ืh_๘KัŽ%r•ฆ-]๚7FฉัK#S๛l‘]=b&mษ„sbๆuHุชŸT่:6แุุ…ฦO<ฏ[(=ขiBใ Xํ2มEaaฝZูuQฑytฮ ‘šuˆคŠ ’็ eณ<‘xส๋% ให๘&<ซ—_ซฏ—jไC]ฤl?!๛ฑž‘ผGํธึ=ำ๙Hfq:`ฤ ภพ0-O,-๑ฏฐ:(}ƒปบ๔Ÿvปๆง›๊ฏห_o๏ช๋ฆห๒งefo ผ๒๋‰yฉ้?ๅ Bำด~ศ€Œ1mUฒ He๛-=Oำ๔ฉv/ๅK๙RžWสrRท iาฯ‘>†,!็ป4พณ4๗๛า็-VตC๏[Mฒช=8Xู•YูยฎVึใ;ข'3D……]Cอล๙ฉํQ5g ชช๐$ Oซ'จ้ฎIฏ;0ฐR—ิ†™[€๖กำ๙P ็ฺt ่ดฑ>ิ7\ข+;๐Ÿ:œ”[ฆใD๛mfC2j์„€nI๊•dผqXบฺ฿—Gwuุ6n๊๕ีn็๖U%ญ๑ฏA๕4A@า๚S"๚Sf๐ูjว_ฒฑš0 ๅง 4๛ู%ท=–b4A€“ฝtlว’EO ไ/‰ะX‰๐ˆpจ,k‡C( k k "€ํึ๐Wฏ.ึๆlmอjี.ิฦ!FCŽ้ํา‰)XQTาx:&ไŒDJVต Pฒ6zy>`uฬL๛’ œภL๙ฝ0ถชใิX1๘ๆq4d –zŒ*แฯก่yะ[๚ฎŽBอ$vค{6:๊oอพฃ;โ]นwWปmsต67‡ฝ–ฅฏ๏M๘)NybBฺŠษ_ณฤฆฦ4Tฟิ฿์ศ 0c(ืงin‚%ๆZส~ RgŒฬS1ฟๆ& mCcูพฐf@Y{ฌ‡#!XX+ณYฏ์8;ภฮซB้Bำ‘ๅฃRGPณ๎๗‰ฺ<˜ฏY9tั(yด>žo”}^7O—ฎ5R฿Tป๏๗‰&OM๓D๛งŸโํฮ็้!๙ํนfR†šงๆ~˜ ฺใ4fฐุ ‰ะx๋M`ภnฟ๔5๎๗๛]๋ํถๅf[yฟwฒึณง$๔dฐ_ˆ—šC_“>๛หญฺฺษkb็‹Cถd„@UFชษJZ-G Z?ภŸŸ[ด}mfO—,ๅBYฒดl_ฒฤ…ๅุ,๖3ธต„n๛1 ยy7ปฃฏ‡วบฦบ๑Pึหส! •][lึึX‹`lWe๏˜ฝว่ๅูญ›hตŒhUxชฯ๎บHพu%(P T^ศgzำ๓h:๊ํJเำ(‘วน๛รPQ …Pv๚!ฺื"@IuอŸถ™5^๛ฯ4ค"ำ>Z!ภ€„N(@ ึฐoŽvฟsWปฝ{8–พjธ_zx’๖S,กจ*„พ˜บ๘Oสค†๚๘"‘€ ‰ุ—ไำr|›B|†@ชภษ?ไXj Y kฏิwIHำvนเฆ@k, zhMa ๗ฏ๑๋Mqyพ2ซ•5ึา*ฅwศๅ‘ไsขœ์‚ๆw_+ิy]’;@s(้ฉu‘6ั่Q.M}ูi„>2ณ?ฮ๒‡ฎแดฎ‰K:ฑDค$O’‡ฉ;ณw™–๏3*ม฿ะ`ะN๋o-๏pญ€ภ;@Dlะc้jจkฟ=V๎แX๛’|ใ—? #ค๕Kย@สbB€ๆ๋ื4˜ูŸ๗๓ซ$€,d<!โโฏ?:m.&Kใ็ฤ„€—€D๘1 44H q๛{ฒ;ภw๖เฑB€ญC่ฟ&ุษกj๐๐v๕ใปWลปืg๖|cอjต2ฦ"J๏=z[ˆwB\ญฏฯci4J S&0 e้eiY*eg๕’2C]‚›ื๙u'}3]ัiYบฒ?ฑL‡qาFฃ_Ÿ|4—.D„0ใ$ย\{<s7t;fb˜bฑ`cฦmW[—Fด>ย฿ดA}ํึA?๕ฏ็^๋7ฝ =8๔P;‡วฺ๙cํ|Y{_9D็้M>Ÿฉ€%‚ภS-๙›B)ซB$E"[2B0‘๔:$”‰•ๅ8T>คญŸrP]R~lm pญ%*็ฐชิฮa;eะAดยขk,ถ0…Y#ส:ค;( ว„h ูซพf)เๅiY–†J๔ฎQOส๒๖h๙\๓๓xภNสŽ_๐ใ๕กุŸQุ@„ร฿ฎฝV฿k๛ƒ–ƒถ/iฦ๚Q๛7พ=6ภ๖€]z/tu8@86฿œปฺีอoทeห๕ฑพVฎq3ษ$ดOำ$‚]บoHืๆ๓เฟ†]34ื_ตอภWŒ,dค DV1‚–สฆ” m qƒฒฟT@ ต฿สjc’าๆุ˜@h-Uใpwฌ๐v๔๗๛ฃ?V›ฆี>ิอo7U๓๕ฑนู5ฮฯRŸ–‹ญ๖งi๛กO๚jf~Ÿวh_๛ำ๙fศ ป2Nวฬ4H—ŒวšŸะภิ฿งƒงฒจวำCฟSWaŸ.Dว@Cศ 0„ชq่U๗ญ€‡ฒมCูเพlฐฌ=”•ƒ๏฿ดณฮึึฌWŒ)Œ5Z๕ฒฯ,คHท†์wอ•"้‡}œZ f4A ำu) วTp`้์šไดmฌMณฏํม˜>ฦ เ(4tmเŸ๖ลY{ ฬn{๗ํ†๎๛mพมัOาฺ'Œ Nไg ถซ๚uุ๗un€I เpŒ`ะทUp+‡X{_67ั๛ูczn5ญ?ล sPญ?ถไo่—า฿ฏYศx,()†˜ฆ๗ŒtF@H–&ๅ…„zฌ๙์—’ฝD1๐ุ>6า64๚Eƒ๚9ˆ‡ชฦม_๏‡ฟ[ฯฟฝ[๛wฏW฿ฟนดo.ึ๖|SปZมไฝŒฬRJ?0$9ร'ฯP๛&ฏู^ `y)ุ>(๛D๓Gึ5I^*H ำ‡<2GŽ“<6iศฆโ ฤ?นน8พ1ะ~ญD๛c~/(!Jfไ=์หvr˜^๐XึซฺcใIื$hฃอ๏ฒD๔wส‡~bซลขCs)ค๗ุW'd@F bๆ่9(ๅcฆtธVฌž˜–s-คธ กL์ฺ)้)๗ะ[๎vผรพฤcํฺ5ภšยถ [ภด3s๖ิnYบfz็nฒUgHf„ฒ“นภสฮส3ๅ`฿/ฒฎ?“ฯ๓q‰ฺ่Kืภษํ!Zผ!>|ย=05ํ๗๛vL}, 3›~ญ๋;๗ บk#=4๐p๔ใ}ำqWื๎ชๆใ}ีlMส๚(‡4ฅซ๛ฅ˜ฉe จYh๛C}*‘-งBำ5 ?ท฿j?mfuV&drž4; ล์t`๛!PK?•ใคบ="TMƒUำเ๎Xšฑยํฑ๒‡า๏หvแ ํก๔๗ปwวwฏ‹๗ฏฮ‹หณ•9[fณ2ฆฐ…ฑถo^o [ข&๗้ญฌฝU€์OLc".รฬๆชๆฏธ$Jšwฑ๕ิฌOปิ]{โฆbeีๅy‰–=@๒>Cu&>๖š๙๐ล>3.๎ƒฬ`๚4ขๅSwม๐ษ฿N$มiใัyฤช๑พ่z็•>F•ํS๒ ‹}ง|ๆwIฤ’้_-้Sd @ฦคX–hื2‘‚R.”—ข๕งh๛!อ]jKJ^สุIuA`”s ด‚ตsPึ ๎ส ๏ป=๘๛}้ทว Uƒ่ๆ ˜ยฎบๅ„ พ•ิ[š4sH'eจF=ณ€~œHจXxณฒD['็N5zK4zี>ถฯฌ8~> ท้ฬฃฦรŠอณmท$ เ?ดQ‡ม"Š ื‡!Uƒpw๐๎บ๙ถช?Wออฎq๛าลHQ#~‰S>๓ำ๐SV }้ฯณ๖†jx๑ศ@ฦR„ˆ,ล\.ฃ‘p ๙๓๓Nี!'ตU:'u|Pษฅ…าี|็<›ถว#^?์๕v็๏vGk ษ~ุื฿ˆ้THษ&3fคoFฆ๕eฎใศาh lnvฎฟฉ๊O๗ตปืPy๘b>ึ๓ํ๓๕%a ล*บ์oА๓U#ป2>'ด jํ_ฑ€@ ๛ZšF์=๘7h{่'izฌ!KcBฟ@6pณไkzl— nชvๅภํฑ‚ฑ๊W๔mœภั_={f๕๗wฏํwฏ.Š7บฌูึฌl}๒nG๊่m๊ฝูXฬ๗'e 960 ข›˜๚i92ขิ% …จQ<๖ๆ๐๖\$อ๓ษฉศ*Qž+oษvธูfฌฯจOGjmฎ5^า์,d๘œฌBY{จ<–๏vฆx3ว>sคD๓Y)ฆญo฿๙d @ฦ้Hี๒Sด๎PR™ะ6ค๑[ะI<คษ‡ฮ‰ต"๕,ษ{ดฅ ชฆ}Uใแ่ฏท{้~๋ฏถ{wป?๚ฑยcทช `๋ฐfํBBฺื5๙“กi๚‹’ด‰ฆฯญSำฝXFาฉษ~ขํรx c๐#ฒˆฺฯะŠ ]Mณoฎ7ƒ้>ฆ๙wวฝyฟฟLื3,้ผV๏b˜ฮโบใ/€๗ฮ8ิ่ฏšๆ็ซฒ๕บฌฏ๊fW:ฌ๏I%$w€Dš๖๏`:‡Ÿj’% ๅ๓ฟ๕b~I๘&Ÿ# งโT‚Œี3ม๓บS„˜ ข งjK;ลซ;นZอทv๗U ฝp๕ฐ๓7ปฝ8”~WึXึ‡๔Bƒƒ๑€๗่ป๕๏AหLฆ ฑUภ…€a฿ฮ๓f‚y%Yฺ„ฬํฬื฿ง‰ฤฯ‡ˆๆงซ๕‰ุฐTเไฯ ‘ุaR7u1ŒBN…“จ[k†๓3xจ<~ผošŸ>•ีฟฎซ๚fWปcๅ|ํ0… %B i๚‹ฺ๊ง๊Z™ ฒ ใ)@์ตj๚)šดฎถ l5ท€๔๕พ>=f=ˆน–ŽW“’+€ปคฑ5์ณvถฆ๎ึพํ<`88”5๎ซ U…๛ฒ๒w๛ƒtฟ๕ฟ฿ปฆ๙๑ํ›โoฏ_๏_]ุทg๖๕ูฦ\nVถ9P˜Uฆฐาํ๋ถ“ษ๖าุ–บ †FO{*ๅIO?YCy3–NŸง1ฝG๑w็™~จ‰f0๘‡c๚Dฮเwpb๏ฎA\๗W2]ฝ๔0`ุบฮิ `Y*๔‡าaU{ti‹i€ฑX€ €/ๆŠ็.iพJิ7KYศx:„ำš€@ฯ๋๗C~EZ?%z)๏‡ภ…€ฯ168ฆเS9% ฐO็œ0-3o"Tuƒwธ๗eำเอn๏ฝนq๏//ํ๗ฏ^?พ{[๛ปทลฟต?๛ใ›ืล๗ฏ/‹๗—็ล๋๓3{ฑ)`ณฒฐฒ`ฦEo่ยB8ng€'@ูZd๎พ:œ๗’Wส%Iํพr๋ž/ฦ3๘ิ{ขง9=w๖Dšษ๙๔ฺุื3M^h]†เถโžไว‡ f€ฌƒว๑Py€VJ|•ภงฐHไ_ภ๔%*ตW-j1aKฐ 4ซ=๊๏๘xoูzmœŸป^ฟถ?พ{Wท๗๏Š๛โฟฟW๘๖อ๊๏o_๛๏./์›‹3{ฑY™๓Uaึ…1…5P0ึ0ฆkะุฮONniO์T(˜ ข๚NmีAšฅx8iRw=๚ี=0„ฉถOญศžพฮZ`ธ'3mDOๆใ๋๛฿h–n๚u : "‚๗Uƒxจะ๏K๏ฅ๗วส๛บA๔๒…,x1๒ฦด”๙!๓?(ว฿<ฒ๑H„ย _ง ้œ้Z๚’ถฯท|ฉ˜kภ)uัs;FาK(sC@ไ“ฌ1}T|ใ4ฮaู4Xี –uƒฒ๔7ป๛ใ๎ฎ๘๙๕k๛ท7ฏ›^ฟถ{ช๘๎ี…}wya฿žŸ7g๛๊lc/6+sฑ^™ณUaึE…5`-ฑณCGฆ=3๕–ƒaแ!Pฌœลq๖JW คLŸ71๗R-1ตำฅy+<ื๘a˜gfฆ๔?n%ร…™์ะ์ฤœ”owศฃa๚N๔ฎ3ฟšvjh…ธ=zฟ=xฟ/…€็ัใไ™แงฅSa@ฒHฺ’e~ยไ๋ ๏ื7…,d<1!€งX8๑K/ฃld๕&VjVฯcฦFzAJๅ่~ฏ๓6Zแ<ฺ'Iญ”๚‘ไจ]๗วƒ?ึ•นูํฏ7kwฑY›7็็ถsุผถ๓ฆ๘แ๕›โ‡ืฏ‹ฟฝ~e฿_^๏/.์๋ณฝฌอf]˜Maกฐล < ึฺA […ฺnP˜fMO้ซc้8ฐ{DฺeบววW>%๚>`oฆ๕๗…๛๑๑๏อ๓Ht๛ฑฬุ ำW†}ู้ca๔ุ‰๖฿พ q‡ป๛ƒsปา๙ช๑ท$bฺพถฏ,0P‹XฒดoŠ๏_ร7G=ฒ๑น#z๎รๆ็๖[* ;_J๎^F๒ฏปภ1ภ2@27ฦฌ…E—'jcชุ—ลถฮ]ˆะ bใ=”u=ิปฒึœญื๎๕ู™}{qiพส๐๚M๑ร›7๖๏ฏ฿?ผy]ํีซโปหK๛โยพ>?ณ—gksY6…5kkLaฑฦ@al๏2ึดด6rqงว"’Nแ์้‘$ฬaP:๋๛ึ)สf๋้ฏจNWpฒv?L"๐ A/าฐถUH๗{zว1ฅm‚a-๊4z3 ๔6Hโ€ ํ*Ž8ฺ™uธ=ขฟ9wทw๎P:_ทั\!MzŽ5| ๙kkคš} }ผ€ŒงAศไLห„ิค๚BZFH๋aŸ# BไŠe@{i†ดฆ~’ฝ$๐>j๎*t!K ต?๖rฮiผGWUะx๏Mcถe‰ืป๖ึพฝธpoฯ/ฬป‹ ^œ7๖อ๙™}svf^mZซภลzmฯืksพ*ฬfต2›ขV……•ฑญ 0uตsื๘ุrชd3k{฿{0c hw;d<ภษZQ8qำ๚ะฦ™ก 7"โ•้…’ั0uŒ‘|9โ~ŸZFƒJ7m>4อว๛ฒนWฎฌฝ๗qJŒJnศOั๘C„Oำๆ‹า๓อ# ŸK„˜(฿J Mเy คI.œO•๚‚L๙ฅ}›BฟTs’ฒ •h€๏๐PW`{mด^YkVEk[˜๕ช0๋b›b5๙z ็๋=_ญอ๙ชw ฌ`]ฌฬบu@a-ฦklปฌngผ๎Rอฟ:;|฿–ยZุฌVๆbฝฒฏฮ6ๆลน๎๒ผxyf฿]žูทk{yfํ๙บตEฝb$แ “ไQH’ำฮำo ฃ0žN%ฒฅฆŒ ๙nฅG๔ฝ้฿‚๗ชa{t๎ำƒkผoš๗Usตซรฑ๖•sฟเคgปOO๙PฟฏE๚k๗ Yด6I}ษ ศ@ฦSC"‘˜ะ๏Ku๑ญถNภ M์๓—๓๕วก—"?็1BAORp`๋]!! †˜fดX(ภN๕nM๐(หPoซ…ทฆขฐfm X…ูkุดคo6ลสlŠดn‰ฌ1ฦ  “/=C๋p(_X ๋ข0g๋•นlฬ๓3๛๎๒ย~wy^|๊ย~๚ข๘๎rc฿_n์‹•}ufํลฦšอสc-€้ธหะG, ง’้-ฟที’?Nปๅ‚ฤ V~Š_ฟํ‡ xoภ9 ต3p(ฟ~ho7e๕ห๕ก๚๓ุlต+็ฝG้m~[็ฯ2?ๆS๔4ญ? ญ๔งE๛งด9C@2>–กใŸ—า๏ศย@Œ๛2!ห€VV๋๏็8๙KB€!ๅbึ€#<™โ&8ๅE‹`|๋ณF๏ฌ7P›ฌฑXุฌตƒฆo;M฿ฺฮไ฿วภhJง้eดp˜0ืฉ๏ฦtื0›ี .ึk๓๊lc฿]œฟฝze|๛บ๘๗ทoŠ›ี{๗บ๘๛๓โ๛ืgลSุตฦ๖_๓G.MU€ษ็ L„่vq2อ‘ (’้?ฮ๋'Sญฟอ๗ฮ@ำX8Vฦ฿ะธซ๋~ฺU?}ฺVŽอกn0`๛‘ฌf๒O LYูo้”?Tฺ ?f@2>R…ษ iคKญ1๒OY&X๊?™—’ฝ4ฅŠZ๚-ษKN1๓?้ ‚ู@uก๚˜VซvฃพK›1 ฒ"๔‚CaญYu.ืg็ๆปหหโว7o์nuต๛ฮ฿สีC๙ฦ๊W๘ท7gล๋๓5`มH„‘๓ไ=ฝ๓†]ฒึ’ฮ#9‰ๆ๏a์ื [pฮ@ีW€w{๏ผo๊_oŽ๕ฯWป๊ื]}ป/]ีxู้ีศ4๖ฬฦO ”L\ะฺžApฦ็†Yp `‹ˆืฝd ()jฺณฆฅh๛šึ๓o†| ฺ9K๊R8ต>ไำŽพำ†์#"‚๏~ฮ{ฌƒฒnฐr ๋uปช๒e้๏Gp,ปฏ'6X7ญ่bŒ5…ต`m๏่"Œ ๔ฺŒ‚A—8“(gบ,Žำ;-ป ฟึP€๗+๐nuณฦ}i๑z๋›_n๕??m๋๘๓ถวงป๚รถู–56…พฅ( วV๕ใŸ•>ฌ}๒7eเฎ>[Yศ๘2ˆ‘,'๐m5U›ีˆู$”กY$ r~่Eส๋BฅL่บกw KสฆŽuj๙ฯ‰T๋‡˜oภ€„nDx(Kฝ฿๛ซํึ_ํ๖n๔ฒ๒ต๓` €ยฦš6†ก0ึ˜ิž๗d>นใ]ฦ้ข>@Lผทเั๚œ[Aีฌ๐PxทG๗๋อฑ๗ๅ๓ฆวว๊ทป‡ๆ๎p๔•sยฬฟd฿J€%๚้W ฉ˜%.CA2พRพฟคฮ˜๕ ี๑ิึงh๎ก—›&4ฤŽXปR_ฆ&1ฏ}Ml-Pนึ ฐ=๑แXโฎฌp_ึxจฌข๗tˆเฑ๛๐1๖แˆ*†=โC<น „o&Z๋ฐ๙ฃ/ภปW@xฌ,ฏทฮ~W6uu_฿7ี?>ตไณ?ธCS‡fiฯ”$Hฆ}M๓O๙5dซišหŒถ?#Yศ๘ˆ™๕cไชำ@i/q-ฤ๊85ธ(ลL#฿˜iVRฺหK๑ท>๙/=…๓b™Eื@l-sP6uฒย๛ใ๏ฅ฿WM๗E= …Y™ยP˜ยCร3๚ ๛ พ๑'M|ฝน฿ๆ~๔+๐~ ฏ ชWธ/ ์ั}ธฏš^๏ช๗๑ถ?oช\Tฟ7ท‡ƒ;6uฏ๙ง>ฟ๙Kม}\๛็‚€$4ส/ๅ;šเ๛ฯิW,d| ฤ4ลฅZ๗+BฬขฐTฐD8%XPปNŠ๋ ีj ต)tS๛ยSผคƒ.lฺ๕ ชฆ]Uแแเฏv{ณ?๘]YใฑŒฑฐ2ซvzb;_ม˜~™>€ฏ"๚ ั๘อิวํ\~฿›๛}ฮะธิอ สชภ๐z็๗U๓ำีถ?”็รง๒??ิฟ฿77๛ฝ;ึ5๚ฉ ๑‡ฬœS,ZlญG6RดTkื7,d|)คZB๛ZZhJ฿)Ak ค”๒1ำ?? .ฑฤศฮaKc‰p๗ุrmg;ื@ู4xจjU5ชหฆต Tตรฺ!VฮcีxฌCpภ๛vํ~}ะ`o่๖ษฑ่4๖็ฝํLตล} เ๕ฎ๑๎หๆ—}ำีCซ๊??]W?ึถอ๑เM‡j๗Lz6B@่ห~ฉม~J๚ฐว๛๓\žอg,d|I<•ำฺ—N๏ •M@(Rฬไ1Bืสฦฮี๒S,Kƒcุ|.|๎Y ำŽ!‚๗็[—ภฑ๔w‡ฃฟูอ่๏ฅ฿Wหั9ํ+ท0€( @ัฎ(ˆด€ะ~๛[โ ภฏฺศ~ฟ‚ฆYAีธ/->ภ_๏œ๛ฎt?]mซtW฿?ฏส๓๑S๕ซซ๊ืปป๚jฟsฒlาษฟ฿.ัน?Eใ—‚Rข5๒๏eA@@2พ4#ฤfHSฺถฤŸฌa)!.%ํS-1ำ~ช"ลล ๕/eNม’Y Ob่ง๐;๏แPU๐p<๚ปo๖ณ;บ๛c…‡สcู ึ†•๙P;็ 4ฎPOใ 4ฺuฟฆ•๏P์*ภ‡#๚›ฝsŸชๆทปC๓ำีถฯOที|ผฎใใง๊WŸ๊฿๎๏šรํ๋ส7^Œ๖ื๎e่งM“๗)ัš ดr`ฬ๏,Tฯy! Œ/ „ด‚่~ส?p_F๚|๐า๖Yvฌํ๓g!์ฌL_ฟL%-ฃ*๚Bบ%้Œ‹ั…ƒผฐŸบ€ไnI ศควจไำฑ}๊€-mั)ฉ}ษmi๘ใ—k็เX7n_5ธ++ฟ++|8Tำรq๗ฺWล๗ฏฮ‹ท็{นYู‹uaฮ kึึฏF„#xpฑvUใ๑X;W฿kwจอพtWปฝ๛pฟkxxhnงƒป=์กฎผว่"?<=$@j‹ค~๑ฏa[mษ_mš_hัŸม2C@2 hB€ข–O›ก_ู’ฺฉB€ึfบฯ ]"zšVภ\ฐB]หฎoHเB'ušฦ”๘๙๖)บแ8$ุล๒ค๛b@,ค๚4ฒ]#ฅ=เผ‡c]แอฑฌธ๏ฏ77๎ปหK๛ซ ๛ซ‹โปหK๛โพ9฿ุW›ต=_ญ์ฆ( ่V@hBํ<–uƒ๛ช๑ปฒ๒๗วฃฟ=]๗๓ท‡ƒป;ร๑่ทU้ue3๚ฅhม)q(!๒_ฒชค5ผrฝ%q.‰ศ@ฦ_…˜–งHƒฃ้ิ"๐ิํŽฝ„๘ฏๅŸ"Pb็/CซSrืฌ˜Ra@ถœ2รCปทก็€ฆkBใ˜…็‹e๏ัU%๊ส๎๗ๆUแ/ื๓๚์ฌฺเซW๖ปหK๛๎โพ9;ณ—๋ต=[ญฬสZc้ดeใ๑Xืธ+kPํa๏๎w{<๘๛ใัํชส—Ms่ผGุ›%RฺฝฤeZ็?•๘C_ŒYBฎจL'" %R…ษtซi‰ะ๏?E[S|๊…’NIŸ Kษ› KGฮัฬ๙k?†ฤc`๛K‚*—ฬแ€˜›Aป^ฬํ”T3ํxฌ\cŽUปช2วา฿Ž๖ใูฮผฺl์ๅfmฮW+ณ. SX ๗ผGจรชqxฌื•฿•ฅ฿Uฅ฿U•?4•/›Q6๕ซMc[šา๖yZˆฅ๙1โO ๘‹ลฐd,D2j,ฑ๐—ณ๔ขง/‚ฅB@ˆเ-ค  œ+YฌR?'‰่C%~_reฒ Hdณ €๖T‚@์Zจkฯ'˜K |ะƒ7Uำ ๓ŽMํ๎ณฒึัO๗Ÿ(๐m>:๔เฐ Ybๅ1าพi„๎i๊=ŽYฏด๒Kด๗ะพิ‡˜$&๔IB7ๅkซ=–C3โ๗3#YศxNHัฌ่๑c-K๑fdฉฎะ~ศ}Šๆห i๐9kฺc ๙‡„ŸpT๒ื„วjฉไr=…าSzฌY=x~H๓iœ๔ฉ้?$„ึืโ R๎]ฦ ศ@ฦsร็$3์A FO)ะดS„€FRย/Eล ฤK€%e ๑,!ญžุuR„—%ฯ•&„|’ษ?E๛o`™ฒ@ฦ#€Œ็ˆฯํHฉ ฎ้‡ดฌTฤLดqฅฆ/ฑฤศ!UsŒอ!ๅลศ?$4คDŽŸjJ>U๐‹MqMyR๒C.ž3๗๓)~ก๕%ำE~K\ นถ๔2d ในโ)-˜Pฯคพ„ตsCšฬ).ทDŽE็‡|๑K…TA๒R๛ +ž/ํŸRV+3๗วHP*ฯ๛ค–ฬ๏ื>๗›โืพ'i๔{”…€G" ฯ_*& v&ค‡^ฦ)u๑2I๕ฤL๒กฟว.pZn‰UA๋_lLด{ง‘tcdž"d„าR๒ตถว,>šฯ_BŸ๖ฅี%@ณคš3แ?ฒ๑๑๎€ง \j~ํ‡ˆใ1ๆิ๔Tณzˆ๘Cš{ˆ๘?‡` ๕Oณ@d›b%ะด๎ิz—\/tN๊3ึ4๒็คา๐%๒ฏ@ท$HฺLฐ•ฦUป'ไ•3ž;ย_€ใๅค-ฐ<ฬWญCW ”๖๙–็ั๖๐}iฅ?บ"_ส—๗ผSWŒ}((๔!Pถฉซ๙A ฮะoษ’วกO$‡พ…j/฿_jŠ t?ๆ๒ˆ k\๛…ˆsฬ’ฃ‡‰ไg€Œ—I•["๔e$โ˜ งฟฐ0ะ็yaห—N!ุqŒฅ…ศิ/หฅฌT>ๅw ๙k็pก€ท%ิ,4ก ลำตˆ%ฤฏ™๛ฅhSญ฿CX˜ฤŒd ใฅ@ณhi)B@Hฐ ฤ฿'ๅ5๒G˜“ฟD\ศŸ;@#๒%š๛ํ?–wสฏวRก”บBBHŠu#U ๗‰pC๑˜4ํTืQOฎ’๋eฉฐ$ส?ล็Š๏?#‘€Œ—„%๎Ž%lސ[@(๙๓๒T‘?-C ‰แ็ว,KH~ฉ๙ (CDsR]Kฌ"1๋€f% น๘3CŠ%@‹%‘4gm†ทะฆฅšOฑฤกพg๒d ใฅ!ี@ห.ด-Ÿึซ ’€วศŸ›ข%หภา‹ 1๚“cญZšDš๏>ๆ6Y๛ภ-B)BภcK€˜้Ÿ›แตฅ~นภ็๚‡ฆžบฏว๑ฤศ@ฦKDช;€ฆำˆQา5„Lฟ=k>%๛€ นจ€@‰'คอK„1๒OM‹Y@ษa?”'ื! ีฒThŠY=x฿่ณย๒๏—FKไฏ}แO[ใฟ‚วiฉๆT! ' /ฉ๎Z6$ h๕[v#7M ˆ‘ฟไBเฺ?ทPื'.mVA ้งฉVPถฉyา๑มลย2  yJŸS nใค˜2อ3ฆ‡4ฅๆTŸJ 6—@2^2žสภ…Zohe–œyy๎ ๕๓4nXขษงะล„€ำ๑๋@ศ๋ปD่„อ*า_พฟ)p1ญ_Š๚wส/5๒?๖;u๚_ชๆŸ ‰€Œ—20JZ_–๏งบ(a‡\!ซ@ŒCฤO๗QhืŸRซ ฉฤŸb๊ๅ|๊‹๕Oำ๙ฑ™๘Shsฬ ๒JๅBม~1฿?ŸŠ'-[๑/u&@ช฿?ดXSh2,d|mˆลhB€๖โี๊ไy1๒K%^F"~d๕ง˜๚SM!๒ž”  ส‚R ฆP฿{m?ลโะฺข๕๏ง"ฏiš๏Ÿ“1ืˆษบ\ ะ1า~ญ๏O„,d|X*P„zฎDPิ= Y จk€zศ:ภ‰_ฒ@H3 –˜๖—h๗ฉf๒‰Rณ„sHฺ>N™%๊ปดๅฯ[๙๓}PSL1๓ท@,๒?ดๆฟึ_พ ้ ศ@ฦืŠ! ‡D๚\ำŽYRผ|Šk€ืmu๒ู!?E๛O%T2‡Hูะ~ศบ “า4W@HฺJป!ฐ‚ฆ๕๗๛)ไŸโ๛็.i?$H Hnษ๔/๕”c--c!ฒ๑5#uบเ)nฉฎะL@s p๒ำfค "K}๚ฺ?œฃ•m—ฉ€2?uVmSฬBำค˜–+i!๓ฟถ่O่‹š ฺว‚N ๚ำ๚œ๑™€ŒฏK„ูไ~Š0ภำS„ฎัsม„21๋€๔ใมƒK…ˆ!p ยqLH:R‚cš|qขYธ ต๛๓ข=_6๔ }‘Qา5 D๔!อ‰ู?EEฦ#‘€ŒoฉBMืHฟO‹MAไฤ4ื€Dฒ6P_์:ภฎแืH%– bˆc@J รภƒ.˜@zH‚„ญ๖์hiKฒะ@š } X›ๆ—j๖‘LศฮxBd ใ[FŠliRฝK"งˆœz47ภ2”<š ็ฦ๊ ld?6ญ/4P"bB๏/€>&า๓ฃฅ=F๛งฤฬ‰[R>์๚า_่sฟK4Œฯ„ุƒ˜‘๑ตย$ค›„ผT๓u(o‰ฆ-™šCšh์:Kฎ#๙Pˆ”…ฤบฅงHะ,!๒ญ™าW )ไ6๏?$pฒื,! ˆPHเฤ›rœ๑Hd ใ[FŠ ฅล4ื~›J)ว)๙ฑ9้ฉไ e–HถZ$}ช ี4Ÿฟด!4ง งฮ๛_"HdRก ?i@l2žYศ๘ึ๑X!@_jH)wชv า!กnˆlS4฿ ส‹D ฑวด๙”๙!kLช คแำฟถšžDกYRL@ฌœ๊]๚‘Ÿะ €”๑ษxฒ‘ั"F๘ZštœB1ยี๊:…๐แ„rฑ๋๓พว๚ข•ศyาพ คู„ดTa ี๔ฏ fฺ}ึZgi๔ŠภษK}R_Bฅe<ฒ‘ัโTK€”žB’)0(๛K4ใSด}H("๖”>ฦฮ[*ล๔bB@,v ต๎ิ~Jโ)ฺ์๓ฟ)ย€fโ}แO๒๛๓~h}…HZฦ! #žZเๅ–šฯiZ*!ฤ "๕ลฺฒคŸK๛Ÿฺg+‡ˆ9Eห_bI0ดุ8H‘>i)@Š   Z\ฎ ๔K%Pzฦ :N4๏k-“Bง ฑ๚bํำ๚ฝ4๏kGL+YR„์C€ะณภก™d!@‹ฌื~!! U(ี‹๒—๚"๕=”–๑ฤศ@FFงh)็ข%ว4ฬ“)B€ึถ%ˆ๗sฉ Bศ1m>ฅl์zš”4 !ฒ็ว!ํ?ี Bย‚t”ล}ฒๆฬ€ŒŒ8NB้งšาSฬฬKM๊ฑฒกvล๚‰๕คZR-q คึณฌ„๚๎๓็ว1๓ฟ6@๚0Pศ: š ‘ๅŸษ! iˆฏœ"$,ัฎ๛ํR"•าCe#˜คŽลาฤ,1K@ช?F๒ฺLา๘…๎+Eศ'žpŠณ ค๗ฅhู์L‘€ŒŒtœ"h้1ำ8_ช‡๊K%%ื ๕!ึฏ”พ„ศ)ˆ}Iนะiๆ~ฉ˜ฐ=e๙฿X,@่+ฯ“สBd_k{จ๏)S๚ํS ! ?ๆBHฑL@`?“3G22พ#„๒Ÿขlช๛K\W"])]2฿Ÿช๕kวZ{NšR‰Ÿ๎?F!=Eห_ฏ™3๙ฟd #ใหโs )๙ฑœROjฺcb่~Š?Eห]sษX๕yš91)=๔ž%ฤ/ต!ิP฿2ž1ฒ‘๑ืโิมว’ฐ๖%JุKฺzJ1@#ๆฅ>ฅV€ฐƒ˜/1V)’ฝ–ŸB๘1๕g๒!ศ@Fฦ_SO "œ็ลส~)Kภ’ภม”๓bํ‹! ช • YR‚๚x[O1๕gเ… ฯ’ฟ8•ฌ1Pืา๑วjก๒งžจฅงไKuC$=„ิ่๘T’ )BฟžถอๆฏYศศx๘.‚ฅ็Fš˜–BๆฉๅizLะฺดŒ†,ddผ|<๖๘sฝžšyZŠkXฉ๙ฑvลำดตT’ส|8!-ใ" /_ย ๐ฅ๊™ูSฆ็=U”9Kษ_ฺ๖๛K-pพึVˆ”อxศ@Fฦทƒ/๑~๊4ร”2ฉSฅ๓S\)ืJm?@œ,ใ‡_BR[N1๏g๒ส€ŒŒo ฯEXz’๙๘’?ล*JŸJx๊ฒฑ๖eอA22พ=|๎๛ฯํ’H!็ิเAบ ๙K๚—Bš)ซ้=ี~Jฒ๖ ! 1|ษ๗D๊Œ„SbR๒Bืy œh—jฎ_*,iOฦWˆโฏn@FFฦ‹ภs––ํ‘JŽ)๙Kpชษ}‰)ฏ๊[ฦ Dถdddคโฏ$๓{Š>uaฃSฆ,j๙Kˆ5E(8ลค*๙gกเBถddd|I|ฉ๘€%ๅžบbd~ ygk@ฦ Yศศศ๘า๘ ๊&8ๅ#?)s๊cuคคgญ=ใI‘]ฯKO]up้๕žK๑žBศBCF222ž5+ค–๛ซ฿…ฦฯไŸฑ๕CŸ‘‘‘ย็beต๙ ์_€ŒŒŒŒGเ’๛ˆ๑[่cฦ#๑า$แŒŒŒŒ%๘+ฟ{๐นI8“|ฦฃ-ง#ฯอฯxฑศ€ŒŒŒฏ_๚ศ_‚ณ€‘๑hd ###ฃลsž‘๑ไศ.€ŒŒŒŒฟ™322222ž ตมƒ?ู๖ะผลpซIENDฎB`‚lemonade-sdk-lemonade-dbde812/docs/assets/marketplace.css000066400000000000000000000344711516551144000236010ustar00rootroot00000000000000/* Marketplace Styles - Professional Design */ /* === Theme Variables === */ :root { /* Grid layout constants */ --mp-card-min-width: 240px; --mp-grid-gap: 12px; --mp-grid-max-columns: 3; /* * Max grid width = just under what would fit (max-columns + 1) columns * Formula: (max-columns + 1) * card-min-width + max-columns * gap - 1px * For 3 columns: 4*240 + 3*12 - 1 = 995px * This ensures we can never fit more than 3 columns */ --mp-grid-max-width: calc( (var(--mp-grid-max-columns) + 1) * var(--mp-card-min-width) + var(--mp-grid-max-columns) * var(--mp-grid-gap) - 1px ); /* Light theme - matches website warm yellow/cream */ --mp-bg-page: #fffbe6; --mp-bg-card: #ffffff; --mp-text-primary: #1a1a1a; --mp-text-secondary: #666666; --mp-text-muted: #999999; --mp-border: rgba(0, 0, 0, 0.08); --mp-accent: #1a1a1a; --mp-accent-hover: #333333; --mp-accent-yellow: #ffe066; --mp-accent-yellow-dark: #ffd43b; --mp-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); --mp-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06); --mp-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.1); --mp-radius-sm: 8px; --mp-radius-md: 12px; --mp-radius-lg: 16px; } [data-theme="dark"] { /* Dark theme - matches desktop app */ --mp-bg-page: #000000; --mp-bg-card: #1a1a1a; --mp-text-primary: #ffffff; --mp-text-secondary: #cccccc; --mp-text-muted: #999999; --mp-border: #333333; --mp-accent: #ffffff; --mp-accent-hover: #cccccc; --mp-accent-yellow: #ffc832; --mp-accent-yellow-dark: #e6b800; --mp-shadow-sm: none; --mp-shadow-md: none; --mp-shadow-lg: none; } /* === Embedded Mode === */ /* Hide scrollbars in embedded mode to match app */ html.embedded, html.embedded body { scrollbar-width: none; /* Firefox */ -ms-overflow-style: none; /* IE and Edge */ } html.embedded::-webkit-scrollbar, html.embedded body::-webkit-scrollbar { display: none; /* Chrome, Safari, Opera */ width: 0; height: 0; } body.embedded { background: var(--mp-bg-page); overflow-x: hidden; /* Prevent horizontal scrollbar */ overflow-y: scroll; /* Use fixed width calculation to prevent layout shift from scrollbar */ width: 100%; box-sizing: border-box; } body.embedded .marketplace-main { overflow: visible; } body.embedded .apps-grid { overflow: visible; /* Uses same max-width as base (inherited from .apps-grid) */ } body.embedded .navbar, body.embedded .footer-placeholder, body.embedded .site-footer { display: none !important; } body.embedded .marketplace-main { padding-top: 0; min-height: 100vh; /* Remove centering in embedded mode - use full width with padding instead */ max-width: none; margin: 0; width: 100%; box-sizing: border-box; } body.embedded .marketplace-hero { padding: 12px 0 12px; } [data-theme="dark"] #marketplace-body { background: #000000 !important; } [data-theme="dark"] #marketplace-body::before { display: none !important; } /* === Main Container === */ .marketplace-main { /* Grid max width (995px) + padding (48px) = 1043px */ max-width: calc(var(--mp-grid-max-width) + 48px); margin: 0 auto; padding: 0 24px 48px; min-height: 60vh; overflow-x: hidden; /* Prevent horizontal scroll */ box-sizing: border-box; } /* === Hero Section === */ .marketplace-hero { padding: 48px 0 48px; /* More spacing before content like news page */ } /* In embedded mode, use compact heading matching app section headers */ body.embedded .marketplace-hero { padding: 12px 0; text-align: left; border-bottom: none; background: #000000; } body.embedded .marketplace-title { font-size: 0.75rem; font-weight: 500; color: #666666; margin: 0; text-transform: uppercase; letter-spacing: 0.5px; } body.embedded .marketplace-subtitle { display: none; } /* Embedded mode: match app font family */ body.embedded { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* Embedded mode: align controls with heading */ body.embedded .marketplace-controls { padding: 0; margin-bottom: 16px; } body.embedded .apps-grid { padding: 0; } /* === Controls === */ .marketplace-controls { display: flex; flex-direction: row; flex-wrap: wrap; /* Allow wrapping on narrow screens */ align-items: center; gap: 12px; margin: 0 0 24px; padding: 0; } /* Search */ .search-container { position: relative; width: 240px; max-width: 100%; flex-shrink: 1; min-width: 120px; } .search-input { width: 100%; padding: 8px 12px 8px 34px; font-size: 0.875rem; border: 1px solid var(--mp-border); border-radius: var(--mp-radius-sm); background: var(--mp-bg-card); color: var(--mp-text-primary); transition: border-color 0.15s ease, box-shadow 0.15s ease; outline: none; } .search-input::placeholder { color: var(--mp-text-muted); } .search-input:focus { border-color: var(--mp-accent); box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.05); } [data-theme="dark"] .search-input { background: #1a1a1a; border-color: #333333; color: #ffffff; } [data-theme="dark"] .search-input::placeholder { color: #666666; } [data-theme="dark"] .search-input:focus { border-color: #555555; box-shadow: none; } /* Embedded mode: match model manager styling exactly */ body.embedded .search-input { padding: 8px 12px 8px 34px; background: #0a0a0a; border: 1px solid #1a1a1a; border-radius: 6px; color: #ffffff; font-size: 0.75rem; } body.embedded .search-input::placeholder { color: #666666; } body.embedded .search-input:focus { border-color: #333333; background: #0f0f0f; } body.embedded .search-input:hover { border-color: #2a2a2a; } body.embedded .search-icon { display: none; } body.embedded .search-input { padding-left: 12px; /* Remove left padding for icon */ } .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; pointer-events: none; opacity: 0.4; color: var(--mp-text-muted); } [data-theme="dark"] .search-icon { color: #666666; } /* Category Filters */ .mp-filter-container { display: flex; flex-direction: row; flex-wrap: wrap; /* Wrap to new row when needed */ gap: 4px; flex: 1; justify-content: flex-start; min-width: 0; /* Allow shrinking below content size */ } .mp-filter-btn { padding: 6px 12px; font-size: 0.8125rem; font-weight: 450; border: none; border-radius: var(--mp-radius-sm); background: transparent; color: var(--mp-text-muted); cursor: pointer; transition: color 0.15s ease, background 0.15s ease; white-space: nowrap; width: auto; box-shadow: none; flex-shrink: 0; /* Prevent buttons from shrinking */ } .mp-filter-btn:hover { color: var(--mp-text-primary); background: transparent; } .mp-filter-btn.active { background: var(--mp-accent); color: var(--mp-bg-card); font-weight: 500; } /* Category Dropdown (embedded mode) - matches search input styling */ .mp-filter-select { padding: 8px 32px 8px 12px; font-size: 0.875rem; font-weight: 400; border: 1px solid var(--mp-border); border-radius: var(--mp-radius-sm); background: var(--mp-bg-card); color: var(--mp-text-primary); cursor: pointer; outline: none; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; transition: border-color 0.15s ease, box-shadow 0.15s ease; margin-left: auto; /* Right-justify */ } .mp-filter-select:hover { border-color: var(--mp-text-muted); } .mp-filter-select:focus { border-color: var(--mp-accent); box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.05); } .mp-filter-select option { background: var(--mp-bg-card); color: var(--mp-text-primary); padding: 8px; } [data-theme="dark"] .mp-filter-select { background-color: #1a1a1a; border-color: #333333; color: #ffffff; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); } [data-theme="dark"] .mp-filter-select:hover { border-color: #555555; } [data-theme="dark"] .mp-filter-select:focus { border-color: #555555; box-shadow: none; } [data-theme="dark"] .mp-filter-select option { background: #1a1a1a; color: #ffffff; } /* Embedded mode: match model manager styling exactly */ body.embedded .mp-filter-select { padding: 8px 28px 8px 12px; background-color: #0a0a0a; border: 1px solid #1a1a1a; border-radius: 6px; color: #666666; font-size: 0.75rem; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); } body.embedded .mp-filter-select:hover { border-color: #2a2a2a; } body.embedded .mp-filter-select:focus { border-color: #333333; background-color: #0f0f0f; } body.embedded .mp-filter-select option { background: #0a0a0a; color: #ffffff; } /* === Apps Grid === */ .apps-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(var(--mp-card-min-width), 1fr)); gap: var(--mp-grid-gap); /* Cap at max columns by limiting grid width */ max-width: var(--mp-grid-max-width); } /* === App Card === */ .app-card { background: var(--mp-bg-card); border: 1px solid var(--mp-border); border-radius: var(--mp-radius-md); padding: 16px; transition: box-shadow 0.2s ease, border-color 0.2s ease; cursor: pointer; text-decoration: none; color: inherit; display: flex; flex-direction: column; gap: 12px; } .app-card:hover { border-color: rgba(0, 0, 0, 0.12); box-shadow: var(--mp-shadow-md); } [data-theme="dark"] .app-card { border-color: #333333; } [data-theme="dark"] .app-card:hover { border-color: #444444; } .app-card-header { display: flex; align-items: center; gap: 12px; } .app-logo { width: 44px; height: 44px; border-radius: var(--mp-radius-sm); object-fit: cover; background: var(--mp-bg-page); flex-shrink: 0; } .app-info { flex: 1; min-width: 0; } .app-name { font-size: 0.9375rem; font-weight: 550; color: var(--mp-text-primary); margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.3; } .app-categories { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; } .category-tag { font-size: 0.6875rem; color: var(--mp-text-muted); text-transform: capitalize; } .category-tag::after { content: " ยท "; } .category-tag:last-child::after { content: ""; } .app-description { font-size: 0.8125rem; color: var(--mp-text-secondary); line-height: 1.5; margin: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .app-links { display: flex; gap: 6px; margin-top: auto; padding-top: 4px; } .app-link { font-size: 0.75rem; padding: 6px 12px; border-radius: 6px; text-decoration: none; transition: background 0.15s ease, border-color 0.15s ease; display: inline-flex; align-items: center; font-weight: 500; border: 1px solid transparent; } .app-link.primary { background: var(--mp-accent); color: var(--mp-bg-card); } .app-link.primary:hover { background: var(--mp-accent-hover); } .app-link.secondary { background: transparent; color: var(--mp-text-secondary); border-color: var(--mp-border); } .app-link.secondary:hover { color: var(--mp-text-primary); border-color: var(--mp-text-muted); } [data-theme="dark"] .app-link.secondary { border-color: #444444; color: #aaaaaa; } [data-theme="dark"] .app-link.secondary:hover { border-color: #666666; color: #ffffff; } /* === States === */ .loading-state, .error-state, .empty-state { display: none; flex-direction: column; align-items: center; justify-content: center; padding: 80px 20px; text-align: center; color: var(--mp-text-secondary); } .loading-state.show, .error-state.show, .empty-state.show { display: flex; } .loading-spinner { width: 24px; height: 24px; border: 2px solid var(--mp-border); border-top-color: var(--mp-accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 12px; } @keyframes spin { to { transform: rotate(360deg); } } .error-icon, .empty-icon { font-size: 2rem; margin-bottom: 12px; opacity: 0.5; } .error-state h2 { font-size: 1rem; font-weight: 500; color: var(--mp-text-primary); margin: 0 0 4px 0; } .error-state p, .empty-state p { font-size: 0.875rem; margin: 0; } .retry-btn { margin-top: 16px; padding: 8px 16px; font-size: 0.8125rem; font-weight: 500; background: var(--mp-accent); color: var(--mp-bg-card); border: none; border-radius: var(--mp-radius-sm); cursor: pointer; transition: background 0.15s ease; } .retry-btn:hover { background: var(--mp-accent-hover); } /* === Responsive === */ /* Embedded mode: use same auto-fit grid (inherits from base) */ /* Phone/narrow screen styles */ @media (max-width: 640px) { .marketplace-main { padding: 0 16px 32px; } .marketplace-hero { padding: 24px 0 16px; } .marketplace-title { font-size: clamp(1.95rem, 8.2vw, 2.4rem); } .search-container { width: 100%; max-width: none; } /* In embedded mode, keep search bar at fixed width */ body.embedded .search-container { width: 240px; max-width: 100%; } .apps-grid { grid-template-columns: 1fr; gap: 10px; } .app-card { padding: 14px; } .app-logo { width: 40px; height: 40px; } } /* Very narrow screens / small embedded panels */ @media (max-width: 400px) { .marketplace-main { padding: 0 10px 32px; } body.embedded .marketplace-controls { padding: 0; } body.embedded .apps-grid { padding: 0; grid-template-columns: 1fr; /* Force single column on very narrow */ } .app-card { padding: 12px; } .app-name { font-size: 0.875rem; } .app-description { font-size: 0.75rem; } .app-link { padding: 5px 10px; font-size: 0.7rem; } } /* === Hide grid when showing states === */ .apps-grid.hidden { display: none; } lemonade-sdk-lemonade-dbde812/docs/assets/marketplace.js000066400000000000000000000172241516551144000234220ustar00rootroot00000000000000/** * Marketplace JavaScript * Handles loading, filtering, and displaying apps */ // Configuration const APPS_JSON_URL = 'https://raw.githubusercontent.com/lemonade-sdk/marketplace/main/apps.json'; // State let allApps = []; let categories = []; let selectedCategory = 'all'; let searchQuery = ''; /** * Check if running in embedded mode (inside Electron app) */ function isEmbedded() { const params = new URLSearchParams(window.location.search); return params.get('embedded') === 'true'; } /** * Check if dark theme is requested */ function isDarkTheme() { const params = new URLSearchParams(window.location.search); return params.get('theme') === 'dark'; } /** * Initialize the page */ function initMarketplace() { // Apply embedded mode if (isEmbedded()) { document.documentElement.classList.add('embedded'); document.body.classList.add('embedded'); } // Apply dark theme if (isDarkTheme()) { document.documentElement.setAttribute('data-theme', 'dark'); } // Setup search const searchInput = document.getElementById('search-input'); if (searchInput) { searchInput.addEventListener('input', debounce(handleSearch, 200)); } } /** * Load apps from the JSON file */ async function loadApps() { const grid = document.getElementById('apps-grid'); const loading = document.getElementById('loading-state'); const error = document.getElementById('error-state'); const empty = document.getElementById('empty-state'); // Show loading grid.classList.add('hidden'); loading.classList.add('show'); error.classList.remove('show'); empty.classList.remove('show'); try { const response = await fetch(APPS_JSON_URL); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); allApps = data.apps || []; categories = data.categories || []; // Render categories renderCategories(); // Render apps renderApps(); // Hide loading loading.classList.remove('show'); grid.classList.remove('hidden'); } catch (err) { console.error('Failed to load apps:', err); loading.classList.remove('show'); error.classList.add('show'); } } /** * Render category filter buttons or dropdown (for embedded mode) */ function renderCategories() { const container = document.getElementById('category-filters'); if (!container) return; // In embedded mode, use a dropdown for space efficiency if (isEmbedded()) { let options = ``; for (const cat of categories) { options += ``; } container.innerHTML = ` `; const select = document.getElementById('category-select'); if (select) { select.addEventListener('change', (e) => { selectedCategory = e.target.value; renderApps(); }); } return; } // Non-embedded: use buttons let html = ``; // Create category buttons for (const cat of categories) { html += ``; } container.innerHTML = html; // Add click handlers container.querySelectorAll('.mp-filter-btn').forEach(btn => { btn.addEventListener('click', () => handleCategoryClick(btn)); }); } /** * Handle category button click */ function handleCategoryClick(btn) { // Update active state document.querySelectorAll('.mp-filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); // Update selected category selectedCategory = btn.dataset.category; // Re-render apps renderApps(); } /** * Handle search input */ function handleSearch(e) { searchQuery = e.target.value.toLowerCase().trim(); renderApps(); } /** * Filter apps based on current state */ function filterApps() { return allApps.filter(app => { // Category filter if (selectedCategory !== 'all') { if (!app.category || !app.category.includes(selectedCategory)) { return false; } } // Search filter if (searchQuery) { const nameMatch = app.name.toLowerCase().includes(searchQuery); const descMatch = app.description.toLowerCase().includes(searchQuery); if (!nameMatch && !descMatch) { return false; } } return true; }); } /** * Render app cards */ function renderApps() { const grid = document.getElementById('apps-grid'); const empty = document.getElementById('empty-state'); const filteredApps = filterApps(); if (filteredApps.length === 0) { grid.classList.add('hidden'); empty.classList.add('show'); return; } grid.classList.remove('hidden'); empty.classList.remove('show'); let html = ''; for (const app of filteredApps) { html += renderAppCard(app); } grid.innerHTML = html; // Prevent card click when clicking links grid.querySelectorAll('.app-link').forEach(link => { link.addEventListener('click', e => e.stopPropagation()); }); // Make cards clickable grid.querySelectorAll('.app-card').forEach(card => { card.addEventListener('click', () => { const url = card.dataset.url; if (url) { window.open(url, '_blank', 'noopener,noreferrer'); } }); }); } /** * Render a single app card */ function renderAppCard(app) { const categoryTags = (app.category || []) .slice(0, 2) .map(cat => `${cat}`) .join(''); // No featured badge - keeping it clean // Build links let linksHtml = ''; if (app.links) { if (app.links.app) { linksHtml += `Visit`; } if (app.links.guide) { linksHtml += `Guide`; } if (app.links.video) { linksHtml += `Video`; } } const logoUrl = app.logo || 'data:image/svg+xml,?'; return `

${escapeHtml(app.name)}

${categoryTags}

${escapeHtml(app.description)}

`; } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Debounce function for search */ function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', initMarketplace); lemonade-sdk-lemonade-dbde812/docs/assets/mkdocs_requirements.txt000066400000000000000000000001011516551144000254020ustar00rootroot00000000000000mkdocs mkdocs-material mkdocs-monorepo-plugin pymdown-extensions lemonade-sdk-lemonade-dbde812/docs/assets/models.css000066400000000000000000000241061516551144000225660ustar00rootroot00000000000000:root { --models-bg: #fffbe6; --models-surface: #ffffff; --models-surface-soft: #fffcf0; --models-text: #1c1c1c; --models-muted: #6f6a5b; --models-border: rgba(43, 35, 19, 0.11); --models-shadow: 0 10px 24px rgba(27, 22, 12, 0.06); --models-sidebar-bg: #f8f3e3; --models-sidebar-border: rgba(84, 72, 32, 0.16); --models-sidebar-width: 292px; --models-layout-gap: 28px; } #models-page-body { color: var(--models-text); } .models-main { width: min(1480px, calc(100vw - 56px)); margin: 0 auto; padding: 0 0 60px; } .models-hero { padding: 50px 0 38px; } .models-layout { display: grid; grid-template-columns: var(--models-sidebar-width) minmax(0, 1fr); gap: var(--models-layout-gap); align-items: start; } .models-sidebar-toggle { display: none; } .models-sidebar { position: sticky; top: 78px; height: fit-content; display: flex; flex-direction: column; gap: 14px; } .models-sidebar-backdrop { display: none; } .models-content, .models-browser { width: 100%; max-width: none; } .models-search-wrap { position: relative; } .models-search-input { width: 100%; padding: 10px 11px 10px 35px; font-size: 0.9rem; transition: border-color 0.2s ease, box-shadow 0.2s ease; } .models-search-input:focus { outline: none; } .models-search-icon { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); color: #9d9d9d; pointer-events: none; } .backend-filter-buttons { display: flex; gap: 5px; flex-direction: column; } .label-filter-buttons { display: flex; flex-wrap: wrap; gap: 7px; } .state-box { border: 1px dashed var(--models-border); border-radius: 11px; padding: 16px; color: var(--models-muted); background: rgba(255, 255, 255, 0.85); margin: 10px 0 12px; } .retry-btn { margin-top: 8px; border: 1px solid var(--models-border); background: #fff; border-radius: 8px; padding: 7px 11px; cursor: pointer; } .models-context { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 8px 0 14px; color: #6e6654; font-size: 0.82rem; } .models-context-prefix { font-size: 0.74rem; letter-spacing: 0.05em; text-transform: uppercase; color: #84785e; } .models-context-pill { border: 1px solid rgba(79, 66, 30, 0.14); background: rgba(255, 255, 255, 0.52); border-radius: 999px; padding: 3px 8px; color: #5f553d; } .models-context-count { margin-left: auto; font-weight: 600; color: #4a402b; } .models-clear-filters-btn { border: none; background: transparent; color: #6d5d38; font-size: 0.8rem; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; padding: 0; } .models-clear-filters-btn:hover { color: #3f3316; } .hot-picks { margin: 8px 0 18px; padding: 12px 14px 14px; border: 1px solid rgba(79, 66, 30, 0.12); border-radius: 12px; background: rgba(255, 255, 255, 0.45); } .hot-picks-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; } .hot-picks-title { margin: 0; font-size: 1.04rem; font-weight: 640; letter-spacing: -0.01em; color: #2f2818; } .hot-picks-link { border: none; background: transparent; color: #6e5e37; font-size: 0.8rem; text-decoration: underline; text-underline-offset: 2px; cursor: pointer; padding: 0; } .hot-picks-link:hover { color: #3e3217; } .hot-picks-subtitle { margin: 6px 0 0; font-size: 0.83rem; color: #736955; } .hot-picks .model-grid { margin-top: 12px; } .model-sections { display: flex; flex-direction: column; gap: 28px; } .model-section { width: 100%; } .model-section-toggle { position: relative; width: 100%; border: none; border-radius: 0; background: transparent; text-align: left; padding: 0; cursor: pointer; transition: opacity 0.2s ease; display: flex; justify-content: space-between; align-items: baseline; gap: 10px; } .model-section-toggle:hover { opacity: 0.88; } .model-section-title-wrap { display: inline-flex; align-items: center; gap: 0; min-width: 0; } .model-section-title { font-size: clamp(1.2rem, 1.5vw, 1.5rem); font-weight: 640; letter-spacing: -0.014em; line-height: 1.1; color: #282115; } .model-section-chevron { display: none; } .model-section-count { font-size: 0.82rem; font-weight: 500; letter-spacing: 0; text-transform: none; color: #7b725e; border: none; border-radius: 0; padding: 0; background: transparent; white-space: nowrap; } .model-section.is-collapsed .model-section-toggle { opacity: 0.86; } .model-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 12px; width: 100%; } .model-section.is-collapsed .model-grid { display: none; } .model-card { background: var(--models-surface); border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 18px; padding: 20px; transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; width: 100%; box-sizing: border-box; box-shadow: 0 8px 20px rgba(32, 25, 8, 0.05); } .model-card:hover { border-color: rgba(82, 68, 29, 0.18); box-shadow: 0 12px 26px rgba(32, 25, 8, 0.09); transform: translateY(-1px); } .model-card-head { display: flex; align-items: start; gap: 8px; margin-bottom: 2px; } .model-name { margin: 0 0 10px; font-size: 1.06rem; line-height: 1.35; letter-spacing: -0.01em; color: #201c14; } .badge-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; } .badge { font-size: 0.72rem; border: 1px solid rgba(101, 87, 47, 0.14); border-radius: 999px; padding: 3px 7px; background: #fdf7e6; color: #6a5f46; } .model-pull-wrap { display: flex; align-items: center; gap: 6px; background: #1a1b1f; color: #f5f5f5; border-radius: 9px; border: 1px solid #2c2d33; padding: 7px 8px 7px 11px; box-shadow: none; } .model-pull { margin: 0; flex: 1 1 auto; min-width: 0; background: transparent; border: none; padding: 3px 0; font-size: 0.76rem; color: inherit; overflow-x: auto; } .model-pull-copy-btn { position: relative; flex: 0 0 auto; border: 1px solid rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.9); border-radius: 9px; line-height: 0; padding: 0.42rem; cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease; } .model-pull-copy-btn svg { width: 0.95rem; height: 0.95rem; display: block; fill: currentColor; } .model-pull-copy-btn:hover, .model-pull-copy-btn:focus { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 224, 102, 0.42); transform: translateY(-1px); } .model-pull-copy-btn.is-copied { color: #ffe680; border-color: rgba(255, 224, 102, 0.55); background: rgba(255, 224, 102, 0.16); } .model-details { margin-top: 14px; border-top: 1px solid rgba(83, 69, 36, 0.1); padding-top: 9px; } .kv-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; } .kv-table td { border-top: 1px solid rgba(83, 69, 36, 0.08); padding: 9px 2px; vertical-align: top; } .kv-table td:first-child { width: 39%; color: #7f7660; } .release-card { display: block; border-radius: 8px; text-decoration: none; color: #2a261a; background: transparent; border: none; padding: 0 0 14px; transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease; } .release-card:hover { background: transparent; box-shadow: none; } .release-label { margin-bottom: 8px; } #release-tag-text { display: block; } .release-tag-value { width: 100%; box-sizing: border-box; text-decoration: none; cursor: inherit; } .hidden { display: none !important; } @media (max-width: 1280px) { :root { --models-sidebar-width: 250px; --models-layout-gap: 24px; } .models-layout { grid-template-columns: var(--models-sidebar-width) minmax(0, 1fr); gap: var(--models-layout-gap); } } @media (min-width: 1021px) { .models-hero { width: calc(100% - (var(--models-sidebar-width) + var(--models-layout-gap))); max-width: 1140px; margin-left: calc(var(--models-sidebar-width) + var(--models-layout-gap)); } .models-sidebar { justify-self: start; } .models-content { max-width: 1140px; margin: 0 auto; } } @media (max-width: 1020px) { .models-main { width: min(1200px, calc(100vw - 34px)); } .models-hero { padding: 36px 0 28px; } .models-layout { grid-template-columns: 1fr; gap: 18px; } .models-sidebar-toggle { display: inline-flex; position: sticky; top: 10px; z-index: 122; } .models-sidebar { position: fixed; left: 0; top: 0; width: min(360px, 86vw); max-width: 100%; height: 100dvh; overflow-y: auto; padding: 14px; gap: 10px; background: var(--models-sidebar-bg); border-right: 1px solid rgba(84, 72, 32, 0.26); box-shadow: 0 24px 40px rgba(0, 0, 0, 0.2); z-index: 130; transform: translateX(calc(-100% - 24px)); transition: transform 0.25s ease; } #models-page-body.models-sidebar-open .models-sidebar { transform: translateX(0); } .models-sidebar-backdrop { display: block; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.36); opacity: 0; pointer-events: none; transition: opacity 0.2s ease; z-index: 125; } #models-page-body.models-sidebar-open .models-sidebar-backdrop { opacity: 1; pointer-events: auto; } .model-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 720px) { .models-main { width: calc(100vw - 24px); } .models-sidebar-toggle { top: 8px; } .models-hero { padding: 30px 0 22px; text-align: center; } .models-subtitle { margin: 0 auto; font-size: 1rem; } .models-context { margin: 6px 0 12px; } .models-context-count { margin-left: 0; } .hot-picks { padding: 10px 10px 12px; } .model-section-toggle { padding: 0; } .model-section-title { font-size: 1.1rem; } .model-section-count { font-size: 0.7rem; padding: 3px 7px; } .model-grid { grid-template-columns: 1fr; } } lemonade-sdk-lemonade-dbde812/docs/assets/models.js000066400000000000000000000454631516551144000224230ustar00rootroot00000000000000const GITHUB_REPO = 'lemonade-sdk/lemonade'; const TAGS_URL = `https://api.github.com/repos/${GITHUB_REPO}/tags?per_page=100`; const RAW_BASE = 'https://raw.githubusercontent.com/lemonade-sdk/lemonade'; const RECIPE_PRIORITY = [ 'llamacpp', 'ryzenai-llm', 'flm', 'whispercpp', 'sd-cpp', 'oga-hybrid', 'oga-npu', 'oga-cpu', 'kokoro' ]; const RECIPE_DISPLAY_NAMES = { llamacpp: 'llama.cpp GPU', 'ryzenai-llm': 'Ryzen AI SW NPU', flm: 'FastFlowLM NPU', whispercpp: 'whisper.cpp', 'sd-cpp': 'stable-diffusion.cpp' }; const state = { tag: null, sourceUrl: null, models: [], search: '', recipe: 'all', label: 'all' }; function hasLabel(details, label) { const labels = Array.isArray(details.labels) ? details.labels : []; return labels.includes(label); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = String(text); return div.innerHTML; } function toTitle(text) { return String(text) .replace(/_/g, ' ') .replace(/-/g, ' ') .replace(/\b\w/g, (ch) => ch.toUpperCase()); } function slugify(text) { return String(text).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); } function getRecipeDisplayName(recipe) { if (!recipe || recipe === 'unknown') return 'Unknown Recipe'; return RECIPE_DISPLAY_NAMES[recipe] || toTitle(recipe); } function parseCheckpoint(name, details) { const raw = details.checkpoint; if (typeof raw !== 'string') { return { repo: null, variant: null, display: '' }; } if (name.endsWith('-FLM')) { return { repo: raw, variant: null, display: raw }; } const split = raw.split(':'); if (split.length > 1 && split[0].includes('/')) { return { repo: split[0], variant: split.slice(1).join(':'), display: raw }; } if (raw.includes('/')) { return { repo: raw, variant: null, display: raw }; } return { repo: null, variant: null, display: raw }; } function modelMatchesCatalogFilters(model) { const { name, details } = model; if (!details.suggested) { return false; } const recipe = details.recipe || 'unknown'; if (state.recipe !== 'all' && recipe !== state.recipe) { return false; } if (!state.search) { return true; } const labels = Array.isArray(details.labels) ? details.labels.join(' ') : ''; const blob = `${name} ${details.checkpoint || ''} ${details.recipe || ''} ${labels}`.toLowerCase(); return blob.includes(state.search); } function modelMatchesFilters(model) { if (!modelMatchesCatalogFilters(model)) { return false; } if (state.label !== 'all' && !hasLabel(model.details, state.label)) { return false; } return true; } function getRecipeValues() { const values = new Set(); state.models.forEach((model) => { if (model.details && model.details.suggested) { values.add(model.details.recipe || 'unknown'); } }); return [...values].sort((a, b) => { const aIndex = RECIPE_PRIORITY.indexOf(a); const bIndex = RECIPE_PRIORITY.indexOf(b); if (aIndex >= 0 && bIndex >= 0) return aIndex - bIndex; if (aIndex >= 0) return -1; if (bIndex >= 0) return 1; return a.localeCompare(b); }); } function getLabelValues() { const values = new Set(['hot']); state.models.forEach((model) => { if (model.details && model.details.suggested && Array.isArray(model.details.labels)) { model.details.labels.forEach((label) => values.add(label)); } }); return [...values].sort((a, b) => a.localeCompare(b)); } function renderRecipeButtons() { const container = document.getElementById('recipe-filter-buttons'); const recipes = getRecipeValues(); const buttons = [{ key: 'all', title: 'All recipes' }, ...recipes.map((recipe) => ({ key: recipe, title: getRecipeDisplayName(recipe) }))]; container.innerHTML = buttons .map((item) => ` `) .join(''); container.querySelectorAll('.backend-filter-btn').forEach((button) => { button.addEventListener('click', () => { state.recipe = button.dataset.recipe; renderRecipeButtons(); renderModels(); }); }); } function renderLabelButtons() { const container = document.getElementById('label-filter-buttons'); const labels = getLabelValues().filter((label) => label !== 'hot' && label !== 'all'); const buttons = [ { key: 'hot', title: '๐Ÿ”ฅ hot' }, { key: 'all', title: 'All labels' }, ...labels.map((label) => ({ key: label, title: label })) ]; container.innerHTML = buttons .map((item) => ` `) .join(''); container.querySelectorAll('.label-filter-btn').forEach((button) => { button.addEventListener('click', () => { state.label = button.dataset.label; renderLabelButtons(); renderModels(); }); }); } function buildModelTableRows(name, details) { const rows = []; const checkpoint = parseCheckpoint(name, details); if (checkpoint.repo) { rows.push({ key: 'Checkpoint', value: `${escapeHtml(checkpoint.repo)}` }); if (checkpoint.variant) { rows.push({ key: 'GGUF Variant', value: escapeHtml(checkpoint.variant) }); } } else if (details.checkpoint) { rows.push({ key: 'Checkpoint', value: escapeHtml(details.checkpoint) }); } for (const [key, value] of Object.entries(details)) { if (['checkpoint', 'max_prompt_length', 'suggested', 'labels'].includes(key)) { continue; } if (key === 'image_defaults' && value && typeof value === 'object') { if (value.steps !== undefined) rows.push({ key: 'Default Steps', value: escapeHtml(value.steps) }); if (value.cfg_scale !== undefined) rows.push({ key: 'Default CFG Scale', value: escapeHtml(value.cfg_scale) }); if (value.width !== undefined && value.height !== undefined) { rows.push({ key: 'Default Size', value: `${escapeHtml(value.width)}x${escapeHtml(value.height)}` }); } continue; } if (key === 'size') { rows.push({ key: 'Size (GB)', value: escapeHtml(value) }); continue; } rows.push({ key: toTitle(key), value: escapeHtml(value) }); } return rows; } function renderModelCard(model, options = {}) { const { compact = false } = options; const { name, details } = model; const labels = Array.isArray(details.labels) ? details.labels : []; const tableRows = buildModelTableRows(name, details); const pullCommand = `lemonade-server pull ${name}`; const badgeMarkup = [ ...labels.map((label) => `${escapeHtml(label)}`) ].join(''); const rowMarkup = tableRows .map((row) => `${escapeHtml(row.key)}${row.value}`) .join(''); return `

${escapeHtml(name)}

${badgeMarkup}
${escapeHtml(pullCommand)}
${compact ? '' : `
${rowMarkup}
`}
`; } function wireSectionToggles() { document.querySelectorAll('.model-section-toggle').forEach((button) => { button.addEventListener('click', () => { const sectionId = button.getAttribute('data-section-id'); const sectionEl = document.getElementById(sectionId); if (!sectionEl) return; const willCollapse = !sectionEl.classList.contains('is-collapsed'); sectionEl.classList.toggle('is-collapsed', willCollapse); const chevron = button.querySelector('.model-section-chevron'); if (chevron) { chevron.textContent = willCollapse ? 'โ–ธ' : 'โ–พ'; } }); }); } function recipeDisplayName(recipe) { return getRecipeDisplayName(recipe); } function labelDisplayName(label) { if (label === 'all') return 'All labels'; if (label === 'hot') return 'Hot'; return toTitle(label); } function wireModelCardCopyButtons() { document.querySelectorAll('.model-pull-copy-btn').forEach((button) => { button.addEventListener('click', async () => { const text = button.dataset.copyText || ''; if (!text) return; try { await navigator.clipboard.writeText(text); } catch (error) { const helper = document.createElement('textarea'); helper.value = text; document.body.appendChild(helper); helper.select(); document.execCommand('copy'); document.body.removeChild(helper); } button.classList.add('is-copied'); button.setAttribute('aria-label', 'Pull command copied'); button.setAttribute('title', 'Pull command copied'); setTimeout(() => { button.classList.remove('is-copied'); button.setAttribute('aria-label', 'Copy pull command'); button.setAttribute('title', 'Copy pull command'); }, 1200); }); }); } function renderModels() { const sectionsHost = document.getElementById('supported-model-sections'); const loading = document.getElementById('models-loading'); const error = document.getElementById('models-error'); const empty = document.getElementById('models-empty'); const context = document.getElementById('models-context'); const hotPicksSection = document.getElementById('hot-picks-section'); const hotPicksGrid = document.getElementById('hot-picks-grid'); const hotPicksTitle = document.querySelector('.hot-picks-title'); loading.classList.add('hidden'); error.classList.add('hidden'); const baseFiltered = state.models.filter(modelMatchesCatalogFilters); const filtered = state.label === 'all' ? baseFiltered : baseFiltered.filter((model) => hasLabel(model.details, state.label)); const hotCandidates = baseFiltered.filter((model) => hasLabel(model.details, 'hot')); const countText = `${filtered.length} model${filtered.length === 1 ? '' : 's'}`; const showClear = state.recipe !== 'all' || state.label !== 'all' || !!state.search; const searchText = state.search ? `Search: "${state.search}"` : ''; context.innerHTML = ` Showing ${escapeHtml(labelDisplayName(state.label))} ${escapeHtml(state.recipe === 'all' ? 'All recipes' : getRecipeDisplayName(state.recipe))} ${searchText ? `${escapeHtml(searchText)}` : ''} ${escapeHtml(countText)} ${showClear ? '' : ''} `; context.classList.remove('hidden'); if (state.label !== 'hot' && hotCandidates.length > 0) { const hotPicks = hotCandidates.slice(0, 6); hotPicksTitle.textContent = 'Hot models'; hotPicksGrid.innerHTML = hotPicks.map((model) => renderModelCard(model, { compact: true })).join(''); hotPicksSection.classList.remove('hidden'); } else { hotPicksGrid.innerHTML = ''; hotPicksSection.classList.add('hidden'); } if (filtered.length === 0) { sectionsHost.classList.add('hidden'); sectionsHost.innerHTML = ''; empty.classList.remove('hidden'); wireModelCardCopyButtons(); return; } empty.classList.add('hidden'); sectionsHost.classList.remove('hidden'); const groupedByRecipe = new Map(); filtered.forEach((model) => { const key = model.details.recipe || 'unknown'; if (!groupedByRecipe.has(key)) groupedByRecipe.set(key, []); groupedByRecipe.get(key).push(model); }); const orderedRecipes = getRecipeValues().filter((recipe) => groupedByRecipe.has(recipe)); if (groupedByRecipe.has('unknown') && !orderedRecipes.includes('unknown')) { orderedRecipes.push('unknown'); } const sectionHtml = []; orderedRecipes.forEach((recipe) => { const models = groupedByRecipe.get(recipe); const cards = models.map(renderModelCard).join(''); const sectionId = `section-recipe-${slugify(recipe)}`; const isCollapsedByDefault = false; const modelCount = `${models.length} model${models.length === 1 ? '' : 's'}`; sectionHtml.push(`
${cards}
`); }); sectionsHost.innerHTML = sectionHtml.join(''); wireSectionToggles(); wireModelCardCopyButtons(); } async function fetchLatestVTag() { const response = await fetch(TAGS_URL); if (!response.ok) { throw new Error(`Tag lookup failed: HTTP ${response.status}`); } const tags = await response.json(); if (!Array.isArray(tags)) { throw new Error('Tag response is invalid'); } const match = tags.find((tag) => typeof tag.name === 'string' && /^v/.test(tag.name)); if (!match) { throw new Error('No v* tag found'); } return match.name; } async function loadModelsData() { const loading = document.getElementById('models-loading'); const error = document.getElementById('models-error'); const sectionsHost = document.getElementById('supported-model-sections'); const empty = document.getElementById('models-empty'); const context = document.getElementById('models-context'); const hotPicksSection = document.getElementById('hot-picks-section'); const hotPicksGrid = document.getElementById('hot-picks-grid'); loading.classList.remove('hidden'); error.classList.add('hidden'); sectionsHost.classList.add('hidden'); empty.classList.add('hidden'); context.classList.add('hidden'); hotPicksSection.classList.add('hidden'); hotPicksGrid.innerHTML = ''; const tag = await fetchLatestVTag(); const sourceUrl = `${RAW_BASE}/${tag}/src/cpp/resources/server_models.json`; const response = await fetch(sourceUrl); if (!response.ok) { throw new Error(`Model JSON fetch failed: HTTP ${response.status}`); } const data = await response.json(); if (!data || typeof data !== 'object' || Array.isArray(data)) { throw new Error('Model JSON payload is invalid'); } state.tag = tag; state.sourceUrl = sourceUrl; state.models = Object.entries(data).map(([name, details]) => ({ name, details })); const releaseTagLink = document.getElementById('release-tag-link'); const releaseTagText = document.getElementById('release-tag-text'); releaseTagText.textContent = `Lemonade ${tag}`; releaseTagLink.href = sourceUrl; releaseTagLink.classList.remove('hidden'); renderRecipeButtons(); renderLabelButtons(); renderModels(); } function wireControls() { const searchInput = document.getElementById('model-search-input'); const retryButton = document.getElementById('retry-load-btn'); const modelsContext = document.getElementById('models-context'); const hotPicksViewAllButton = document.getElementById('hot-picks-view-all-btn'); const sidebarToggle = document.getElementById('models-sidebar-toggle'); const sidebar = document.getElementById('models-sidebar'); const sidebarBackdrop = document.getElementById('models-sidebar-backdrop'); const pageBody = document.getElementById('models-page-body'); function setSidebarOpen(isOpen) { if (!sidebarToggle || !sidebar || !sidebarBackdrop || !pageBody) return; pageBody.classList.toggle('models-sidebar-open', isOpen); sidebarToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); sidebarBackdrop.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); } searchInput.addEventListener('input', () => { state.search = searchInput.value.trim().toLowerCase(); if (state.label === 'hot') { state.label = 'all'; renderLabelButtons(); } renderModels(); }); retryButton.addEventListener('click', () => { initializeModelsPage(); }); if (hotPicksViewAllButton) { hotPicksViewAllButton.addEventListener('click', () => { state.label = 'hot'; renderLabelButtons(); renderModels(); const sections = document.getElementById('supported-model-sections'); if (sections) { sections.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); } if (modelsContext) { modelsContext.addEventListener('click', (event) => { const clearButton = event.target.closest('#models-clear-filters-btn'); if (!clearButton) return; state.recipe = 'all'; state.label = 'all'; state.search = ''; searchInput.value = ''; renderRecipeButtons(); renderLabelButtons(); renderModels(); }); } if (sidebarToggle && sidebar && sidebarBackdrop && pageBody) { sidebarToggle.addEventListener('click', () => { const willOpen = !pageBody.classList.contains('models-sidebar-open'); setSidebarOpen(willOpen); }); sidebarBackdrop.addEventListener('click', () => { setSidebarOpen(false); }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { setSidebarOpen(false); } }); sidebar.addEventListener('click', (event) => { const targetButton = event.target.closest('.backend-filter-btn, .label-filter-btn'); if (targetButton && window.matchMedia('(max-width: 1020px)').matches) { setSidebarOpen(false); } }); window.addEventListener('resize', () => { if (window.innerWidth > 1020) { setSidebarOpen(false); } }); } } async function initializeModelsPage() { const loading = document.getElementById('models-loading'); const error = document.getElementById('models-error'); try { if (!window.__modelsPageControlsBound) { wireControls(); window.__modelsPageControlsBound = true; } await loadModelsData(); } catch (err) { console.error('Failed to initialize models page:', err); loading.classList.add('hidden'); error.classList.remove('hidden'); } } lemonade-sdk-lemonade-dbde812/docs/assets/navbar.js000066400000000000000000000017431516551144000224020ustar00rootroot00000000000000// Shared Navbar Component for Lemonade docs pages function createNavbar(basePath = '') { return ` `; } function initializeNavbar(basePath = '') { const navbarContainer = document.querySelector('.navbar-placeholder'); if (navbarContainer) { navbarContainer.innerHTML = createNavbar(basePath); } else { console.warn('Navbar placeholder not found'); } } if (typeof module !== 'undefined' && module.exports) { module.exports = { createNavbar, initializeNavbar }; } lemonade-sdk-lemonade-dbde812/docs/assets/news-data.js000066400000000000000000000457701516551144000230240ustar00rootroot00000000000000// News content data - easy to add new entries const newsData = [ { title: "Lemonade by AMD: A Unified API for Local AI Developers", url: "https://www.amd.com/en/developer/resources/technical-articles/2026/lemonade-for-local-ai.html", date: "February 10, 2026", description: "Learn how Lemonade delivers a unified, local-first API for text, image, and speech AI across Ryzen AI, Radeon, and broader AI PC hardware.", image: "https://www.amd.com/content/dam/amd/en/images/blogs/designs/technical-blogs/lemonade-sdk/lemonadeheadergraphicsized.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "AMD puts Strix Halo loaded with 8 AI models in a bun-fight: Is a hot dog a sandwich?", url: "https://videocardz.com/newz/amd-puts-strix-halo-loaded-with-8-ai-models-in-a-bun-fight-is-a-hot-dog-a-sandwich", date: "December 24, 2025", description: "VideoCardz covers AMD Strix Halo running multiple AI models locally and explores performance and positioning for AI PCs.", image: "https://cdn.videocardz.com/1/2025/12/RYZEN-AI-MAX-SANDWICH-HOTDOG-1-1200x624.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "Ryzen AI and Radeon are ready to run LLMs Locally", url: "https://www.amd.com/en/developer/resources/technical-articles/2025/ryzen-ai-radeon-llms-with-lemonade.html", date: "November 13, 2025", description: "Learn about LLM use cases that run on a variety of AMD PCs.", image: "https://www.amd.com/content/dam/amd/en/images/blogs/designs/generic-thumbnails/9851-industry-applications-use-cases-02.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "Lemonade for GitHub Copilot", url: "https://youtu.be/HUwGxlH3yAg?si=0P-S48_PgrWy9WwI", date: "October 14, 2025", description: "Watch how Lemonade brings local AI coding power to VS Code GitHub Copilot Chatโ€”optimized for AMD Ryzenโ„ข AI PCs.", image: "https://img.youtube.com/vi/HUwGxlH3yAg/maxresdefault.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "video" }, { title: "Dify + Lemonade for Agentic Workflows", url: "https://www.amd.com/en/developer/resources/technical-articles/2025/harnessing-dify-and-local-llms-on-ryzen-ai-pcs-for-private-workf.html", date: "October 6, 2025", description: "Lemonade now available with Dify, enabling private AI workflows to run locally. Build powerful LLM + RAG apps without cloud dependencies or ML expertise.", image: "https://www.amd.com/content/dam/amd/en/images/blogs/designs/technical-blogs/harnessing-dify---local-llms-on-amd-ryzen-ai-pcs-for-private-workflows/DifyChatExample.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "Introducing Lemonade Arcade", url: "https://lemonade-server.ai/news/lemonade-arcade.html", date: "August 22, 2025", description: "We're excited to announce Lemonade Arcade, a new application that transforms your GPU into a creative engine for generating retro-style games using AI.", image: "https://github.com/lemonade-sdk/lemonade-arcade/blob/main/img/banner.png?raw=true", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "Run OpenAI's gpt-oss locally with Lemonade", url: "https://lemonade-server.ai/news/gpt-oss.html", date: "August 12, 2025", description: "Lemonade now supports OpenAI's gpt-oss models, bringing you the power to run these cutting-edge models locally on your own hardware! ๐ŸŽ‰", image: "https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/banner.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "llamacpp+ROCm7 beta is now supported on Lemonade", url: "https://www.reddit.com/r/LocalLLaMA/comments/1mjgj2x/llamacpprocm7_beta_is_now_supported_on_lemonade/", date: "August 6, 2025", description: "Unlock the power of ROCm7 on your STX Halo or Radeon GPU with Lemonade 8.1.1.", image: "https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/rocm_hero.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "AMD, ISV Security Partners Collaborate to Protect the AI PC", url: "https://www.amd.com/en/blogs/2025/isv-security-experiences-with-Ryzen-PRO.html", date: "August 05, 2025", description: "Lemonade Server and Ryzenโ„ข AI NPUs bring real-time, on-device protection against phishing, deepfakes, and prompt attacks. No cloud lag, just smarter security where it counts.", image: "https://www.amd.com/content/dam/amd/en/images/blogs/designs/projects/isv-commercial-security-experiences-blog/security-shield-key-art.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "Lemonade Server & Open WebUI", url: "https://www.youtube.com/watch?v=yZs-Yzl736E", date: "July 31, 2025", description: "Easily integrate Lemonade Server with Open WebUI to unlock powerful local LLM capabilities on your PC. This video guides you through the installation and setup process.", image: "https://img.youtube.com/vi/yZs-Yzl736E/maxresdefault.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "video" }, { title: "Styrk AI & AMD: Guardrails for Your On-Device AI Revolution", url: "https://styrk.ai/styrk-ai-and-amd-guardrails-for-your-on-device-ai-revolution/", date: "July 14, 2025", description: "AMD and Styrk AI bring real-time, on-device LLM security. Powered by AMD NPUs, GPUs, and Lemonade Server, with built-in guardrails for filtering, adversarial detection, and prompt injection defense.", image: "https://styrk.ai/wp-content/uploads/2025/07/styrk-ai_amd-768x432.webp", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "AMD and XMPro Deliver Autonomous Intelligence at the Edge", url: "https://www.amd.com/en/developer/resources/technical-articles/2025/empowering-local-industrial-compute.html", date: "July 15, 2025", description: "AMD and XMPro have partnered to bring advanced AI capabilities to the industrial edge using AMD hardware and the Lemonade Server for efficient, private, local AI workloads.", image: "https://www.amd.com/en/developer/resources/technical-articles/2025/empowering-local-industrial-compute/_jcr_content/_cq_featuredimage.coreimg.jpeg/1752678519383/3598267-amd-x-xmpro-image-edit-1200x627-no-copy.jpeg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "Rethinking Local AI: Lemonade Serverโ€™s Python Advantage", url: "https://www.amd.com/en/developer/resources/technical-articles/2025/rethinking-local-ai-lemonade-servers-python-advantage.html", date: "July 21, 2025", description: "Learn about why we chose Python for deploying local LLMs with Lemonade and how integrating with your app is incredibly easy.", image: "https://www.amd.com/content/dam/amd/en/images/blogs/designs/technical-blogs/rethinking-local-ai/figure%203%20our%20approach%20to%20python%20gives%20us%20both%20development%20agility%20and%20the%20expected%20level%20of%20production%20readines.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "Minions: On-Device and Cloud Language Model Collaboration on AMD Ryzenโ„ข AI", url: "https://www.amd.com/en/developer/resources/technical-articles/2025/minions--on-device-and-cloud-language-model-collaboration-on-ryz.html", date: "July 08, 2025", description: "Minions, a new framework from Stanfordโ€™s Hazy Research Group, lets cloud models collaborate with lighter ones on-deviceโ€”and now runs on Ryzen AI via AMDโ€™s Lemonade Server.", image: "https://www.amd.com/content/dam/amd/en/images/blogs/designs/technical-blogs/minions-on-device-and-cloud-language-model-collaboration-on-ryzen-ai/3546073_Minions_Blog_Banner_1200x600.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "Local Tiny Agents: MCP Agents on Ryzenโ„ข AI with Lemonade Server", url: "https://www.amd.com/en/developer/resources/technical-articles/2025/local-tiny-agents--mcp-agents-on-ryzen-ai-with-lemonade-server.html", date: "June 10, 2025", description: "Model Context Protocol (MCP) is now available on AMD Ryzenโ„ข AI PCs and can be used by installing AMD Lemonade Server and connecting it to projects like Hugging Face's Tiny Agents via streaming tool calls.", image: "https://www.amd.com/content/dam/amd/en/images/blogs/designs/technical-blogs/local-tiny-agents--mcp-agents-on-ryzen-ai-with-lemonade-server/lemonade.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "Unlocking a Wave of LLM Apps on Ryzenโ„ข AI through Lemonade Server", url: "https://www.amd.com/en/developer/resources/technical-articles/unlocking-a-wave-of-llm-apps-on-ryzen-ai-through-lemonade-server.html", date: "April 17, 2025", description: "Lemonade Server enables LLM acceleration on Windows and Linux without code changesโ€”using hybrid NPU+iGPU execution on Ryzenโ„ข AI 300-series PCs and GPU acceleration on Linux.", image: "https://www.amd.com/content/dam/amd/en/images/blogs/designs/projects/technical-blogs/3328050-lemonade-server/3328050-lemonade-server-blog.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "LLMs on AMD Ryzenโ„ข AI PCs", url: "https://www.youtube.com/watch/qMdMJF89c8g", date: "March 31, 2025", description: "The video explains how Ryzenโ„ข AI 300-series PCs use NPUs and integrated GPUs to accelerate LLMs through hybrid task partitioning, and introduces the Ryzen AI Software Stack.", image: "https://img.youtube.com/vi/qMdMJF89c8g/maxresdefault.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "video" }, { title: "Running LLMs on AMD Ryzenโ„ข AI PCs using Lemonade SDK", url: "https://www.youtube.com/watch/Ys7n5OouwtI", date: "March 31, 2025", description: "Watch how the Lemonade SDK lets you experiment with LLMs on Ryzenโ„ข AI 300-series PCs using high-level APIs, including setup and prompting via its CLI.", image: "https://img.youtube.com/vi/Ys7n5OouwtI/maxresdefault.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "video" }, { title: "Using the Lemonade SDK", url: "https://www.youtube.com/watch/1_QU_w1zF7Y", date: "March 31, 2025", description: "Watch to see how the Lemonade SDK can be used to run performance benchmarks and evaluate model accuracy using the MMLU test suite.", image: "https://img.youtube.com/vi/1_QU_w1zF7Y/maxresdefault.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "video" }, { title: "Integrating Lemonade into your Python App", url: "https://www.youtube.com/watch/aeHRGzxxYRQ", date: "March 31, 2025", description: "This video shows how to use the Lemonade SDK to integrate LLMs into an app, enhancing a basic search tool and running it locally on Ryzenโ„ข AI 300-series PCs.", image: "https://img.youtube.com/vi/aeHRGzxxYRQ/maxresdefault.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "video" }, { title: "Lemonade Server + Open WebUI", url: "https://www.youtube.com/watch/PXNTDZREJ_A", date: "March 31, 2025", description: "Introducing Lemonade Server with Open WebUI integration. See how easy it is to install Lemonade Server, download models and get Open WebUI running LLMs on your local PC.", image: "https://img.youtube.com/vi/PXNTDZREJ_A/maxresdefault.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "video" }, { title: "Lemonade Server + Microsoft AI Toolkit", url: "https://www.youtube.com/watch/JecpotOZ6qo", date: "April 24, 2025", description: "Introducing Lemonade Server with Microsoft AI Toolkit integration. See how easy it is to install Lemonade Server, download models and get Microsoft AI Toolkit running LLMs on your local PC.", image: "https://img.youtube.com/vi/PXNTDZREJ_A/maxresdefault.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "video" }, { title: "Lemonade Server + Continue AI Coding Assistant", url: "https://www.youtube.com/watch/bP_MZnDpbUc", date: "April 30, 2025", description: "Introducing Lemonade Server with Continue AI Coding Assistant integration. See how easy it is to install Lemonade Server, download models and get Continue AI Coding Assistant running LLMs on your local PC.", image: "https://img.youtube.com/vi/bP_MZnDpbUc/maxresdefault.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "video" }, { title: "Lemonade Server + PEEL for Local LLM Support in PowerShell", url: "https://www.youtube.com/watch/A-8QYktB0Io", date: "June 6, 2025", description: "Introducing Lemonade Server with PEEL for Local LLM Support in PowerShell integration. See how easy it is to install Lemonade Server, download models and get PEEL running LLMs on your local PC.", image: "https://img.youtube.com/vi/A-8QYktB0Io/maxresdefault.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "video" }, { title: "Introducing Lemonade Server for Local LLM Server with GPU and NPU Acceleration", url: "https://www.youtube.com/watch/mcf7dDybUco", date: "July 14, 2025", description: "See how to install Lemonade Server and run LLMs locally with GPU and NPU acceleration on your PCโ€”no code changes needed.", image: "https://img.youtube.com/vi/mcf7dDybUco/maxresdefault.jpg", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "video" }, { title: "Hugging Face MCP Course: https://huggingface.co/learn/mcp-course/unit2/lemonade-server", url: "https://huggingface.co/learn/mcp-course/unit2/lemonade-server", date: "July 8, 2025", description: "AMD Partnered with Hugging Face to provide a guide on how to accelerate our end-to-end Tiny Agents application using AMD Neural Processing Unit (NPU) and integrated GPU (iGPU).", image: "https://huggingface.co/datasets/mcp-course/images/resolve/main/unit0/1.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "tutorial" }, { title: "GAIA: An Open-Source Project from AMD for Running Local LLMs on Ryzenโ„ข AI", url: "https://www.amd.com/en/developer/resources/technical-articles/gaia-an-open-source-project-from-amd-for-running-local-llms-on-ryzen-ai.html", date: "March 20, 2025", description: "GAIA is an application with multiple agents that runs local LLMs on Ryzenโ„ข AI using Lemonade Server.", image: "https://www.amd.com/content/dam/amd/en/images/blogs/designs/projects/technical-blogs/3328050-lemonade-server/3328050-lemonade-server-blog.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "blog" }, { title: "Lemonade Server v8.1.0 Release", url: "https://github.com/lemonade-sdk/lemonade/releases/tag/v8.1.0", date: "July 30, 2025", description: "Support for Ryzenโ„ข AI Software v1.5.0 and NPU-only execution.", image: "https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/banner.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "release" }, { title: "Lemonade Server v8.0.6 Release", url: "https://github.com/lemonade-sdk/lemonade/releases/tag/v8.0.6", date: "July 17, 2025", description: "Overhauled llamacpp support in the Lemonade Developer CLI.", image: "https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/banner.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "release" }, { title: "Lemonade Server v8.0.5 Release", url: "https://github.com/lemonade-sdk/lemonade/releases/tag/v8.0.5", date: "July 14, 2025", description: "Added device enumeration capability on Windows and Linux to `lemonade system-info` command.", image: "https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/banner.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "release" }, { title: "Lemonade Server v8.0.4 Release", url: "https://github.com/lemonade-sdk/lemonade/releases/tag/v8.0.4", date: "July 09, 2025", description: "Added `reranking` and `embeddings` support to Lemonade Server.", image: "https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/banner.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "release" }, { title: "Lemonade Server v8.0.3 Release", url: "https://github.com/lemonade-sdk/lemonade/releases/tag/v8.0.3", date: "June 27, 2025", description: "Support for large (sharded) GGUF models. Added `Llama-4-Scout-17B-16E-Instruct-GGUF` to server models list.", image: "https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/banner.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "release" }, { title: "Lemonade Server v8.0.0 Release", url: "https://github.com/lemonade-sdk/lemonade/releases/tag/v8.0.0", date: "June 19, 2025", description: "Major release with Ubuntu support, model manager, and Windows tray app.", image: "https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/banner.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "release" }, { title: "Lemonade Server v7.0.2 Release", url: "https://github.com/lemonade-sdk/lemonade/releases/tag/v7.0.2", date: "June 02, 2025", description: "lm-evaluation-harness is now fully integrated as an automated Lemonade CLI tool.", image: "https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/banner.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "release" }, { title: "Lemonade Server v7.0.1 Release", url: "https://github.com/lemonade-sdk/lemonade/releases/tag/v7.0.1", date: "May 30, 2025", description: "Added support for GGUF models and llama.cpp backend to Lemonade Server.", image: "https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/banner.png", imageStyle: "width: 100%; height: 100%; object-position: center top; ", type: "release" }, ]; lemonade-sdk-lemonade-dbde812/docs/assets/news.css000066400000000000000000000063171516551144000222630ustar00rootroot00000000000000:root { --news-shell-width: min(1480px, calc(100vw - 56px)); --news-sidebar-width: 292px; --news-gap: 28px; } .news-sidebar { top: 150px; left: max(16px, calc((100vw - var(--news-shell-width)) / 2)); width: var(--news-sidebar-width); height: auto; background: transparent; border-right: none; box-shadow: none; padding: 0; z-index: 110; } .news-sidebar .sidebar-card { border: none; border-bottom: 1px solid rgba(77, 65, 31, 0.16); border-radius: 0; background: transparent; padding: 0 0 14px; margin-bottom: 14px; } .news-sidebar .sidebar-card:last-child { border-bottom: none; margin-bottom: 0; } .sidebar-header { margin-bottom: 0; } .sidebar-header p { margin: 0; font-size: 0.84rem; color: #6f6a5b; } .filter-container { gap: 5px; margin: 0; } .sort-container { margin: 0; } .sort-label { display: none; } .news-view { width: var(--news-shell-width); max-width: none; margin: 0 auto; padding: 42px 0 44px calc(var(--news-sidebar-width) + var(--news-gap)); box-sizing: border-box; } .news-view .hero-section { margin: 0 auto 26px; padding: 0; } .news-view .main-heading, .news-view .subtitle { animation: none !important; } .news-view .subtitle { margin-bottom: 28px; } .news-posts { gap: 16px; } .news-tile { border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 14px; box-shadow: 0 8px 20px rgba(32, 25, 8, 0.05); aspect-ratio: auto; height: auto; } .news-tile:hover { transform: translateY(-2px); box-shadow: 0 12px 26px rgba(32, 25, 8, 0.09); } .news-tile-link { height: auto; } .news-image { height: auto; aspect-ratio: 16 / 9; overflow: hidden; } .news-image img { width: 100%; height: 100%; object-fit: cover; } .news-content h3 { overflow-wrap: anywhere; } .sidebar-toggle { display: none; } @media (max-width: 1020px) { :root { --news-shell-width: min(1200px, calc(100vw - 34px)); } .news-view { padding-left: calc(var(--news-sidebar-width) + 20px); } } @media (max-width: 768px) { :root { --news-shell-width: calc(100vw - 24px); } .news-sidebar { top: 0; left: 0; width: min(360px, 86vw); height: 100dvh; background: #f8f3e3; transform: translateX(calc(-100% - 24px)); transition: transform 0.25s ease; border-right: 1px solid rgba(84, 72, 32, 0.26); box-shadow: 0 24px 40px rgba(0, 0, 0, 0.2); z-index: 130; padding: 14px; } .news-sidebar.mobile-open { transform: translateX(0); } .news-sidebar .sidebar-card { margin-bottom: 10px; padding-bottom: 10px; } .sidebar-overlay { display: block; opacity: 0; pointer-events: none; background: rgba(0, 0, 0, 0.36); transition: opacity 0.2s ease; z-index: 125; } .sidebar-overlay.active { opacity: 1; pointer-events: auto; } .sidebar-toggle { display: inline-flex; position: sticky; top: 10px; z-index: 122; align-self: flex-start; } .news-view { width: var(--news-shell-width); padding: 30px 0 36px; display: flex; flex-direction: column; gap: 18px; } .news-view .hero-section { text-align: center; margin-bottom: 0; } } @media (max-width: 720px) { .sidebar-toggle { top: 8px; } } lemonade-sdk-lemonade-dbde812/docs/assets/website-styles.css000066400000000000000000003155661516551144000243030ustar00rootroot00000000000000/* Lemonade Website Styles */ /* === CSS Variables === */ :root { /* Colors */ --primary-yellow: #ffe066; --primary-yellow-dark: #ffd43b; --primary-yellow-darker: #ffcc00; --accent-gold: #e6b800; --accent-gold-dark: #bfa100; --text-primary: #222; --text-secondary: #555; --text-muted: #666; --text-light: #999; --text-success: #4ca64c; --bg-primary: #fffbe9; --bg-secondary: #fff8dd; --bg-tertiary: #fff5d1; --bg-white: #fff; --bg-overlay: rgba(255, 251, 233, 0.95); --border-light: #e6e6e6; --border-medium: #ccc; --border-primary: #e6b800; --shadow-light: 0 2px 8px rgba(230, 184, 0, 0.3); --shadow-medium: 0 4px 12px rgba(230, 184, 0, 0.4); --shadow-heavy: 0 8px 24px rgba(0, 0, 0, 0.15); /* Spacing */ --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; --spacing-md: 1rem; --spacing-lg: 1.5rem; --spacing-xl: 2rem; --spacing-xxl: 3rem; /* Typography */ --font-family: 'Inter', 'Segoe UI', 'Arial', sans-serif; --font-size-xs: 0.75rem; --font-size-sm: 0.85rem; --font-size-base: 1rem; --font-size-lg: 1.2rem; --font-size-xl: 1.5rem; --font-size-xxl: 2rem; --font-size-xxxl: 2.5rem; /* Transitions */ --transition-fast: 0.2s ease; --transition-medium: 0.3s ease; --transition-slow: 0.4s ease; /* Border radius */ --radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px; --radius-xl: 18px; --radius-xxl: 24px; --radius-round: 50%; } /* === Base Styles === */ * { box-sizing: border-box; } html, body { overflow-x: hidden; width: 100%; } body { margin: 0; font-family: var(--font-family); background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 50%, var(--bg-tertiary) 100%); color: var(--text-primary); min-height: 100vh; display: flex; flex-direction: column; scroll-behavior: smooth; } body::before { content: ''; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle at 20% 20%, rgba(255, 224, 102, 0.1) 0%, transparent 50%), radial-gradient(circle at 80% 80%, rgba(255, 212, 59, 0.1) 0%, transparent 50%); pointer-events: none; z-index: -1; } body.install-options { align-items: center; justify-content: flex-start; } /* === Navigation === */ .navbar { display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-md) var(--spacing-xxl) var(--spacing-sm) var(--spacing-md); font-size: 1.25rem; font-weight: 500; background: transparent; letter-spacing: 0.02em; position: relative; transition: var(--transition-medium); } .navbar-brand { display: flex; align-items: center; } .brand-title { font-size: var(--font-size-xl); font-weight: 700; color: var(--text-primary); text-decoration: none; letter-spacing: 0.01em; } .navbar-context { position: absolute; left: var(--spacing-xl); font-size: 0.95rem; color: var(--text-muted); display: none; align-items: center; white-space: nowrap; } .navbar-context.show { display: flex; } .navbar-links { display: flex; gap: 2.5rem; align-items: center; font-size: 1.22rem; line-height: 1.1; } .navbar a { color: #444; text-decoration: none; transition: var(--transition-fast); } .navbar-links a, .navbar-brand .brand-title a { display: inline-flex; align-items: center; line-height: 1.15; } .navbar-links a { font-size: 1.22rem; font-weight: 500; } .navbar a:hover { color: var(--accent-gold); } .navbar-back { color: var(--accent-gold); text-decoration: none; font-weight: 500; transition: var(--transition-fast); cursor: pointer; margin-right: var(--spacing-sm); } .navbar-back:hover { color: var(--accent-gold-dark); } /* Breadcrumb */ .breadcrumb { position: fixed; top: 0; left: 0; width: 100%; background: var(--bg-overlay); backdrop-filter: blur(10px); border-bottom: 1px solid rgba(230, 184, 0, 0.2); padding: 0.8rem var(--spacing-xl); z-index: 200; display: flex; align-items: center; font-size: 0.95rem; color: var(--text-muted); min-height: 60px; } .breadcrumb-back { color: var(--accent-gold); text-decoration: none; font-weight: 500; transition: var(--transition-fast); cursor: pointer; white-space: nowrap; } .breadcrumb-back:hover { color: var(--accent-gold-dark); } .breadcrumb-separator { margin: 0 0.8rem; color: var(--border-medium); white-space: nowrap; } /* === Layout Components === */ .main { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; min-height: 60vh; padding: var(--spacing-xl) var(--spacing-md) 4rem var(--spacing-md); max-width: 1200px; width: 100%; margin: 0 auto; position: relative; overflow-x: hidden; } .main > * { animation: fadeInUp 0.6s ease-out both; } .main > *:nth-child(1) { animation-delay: 0.05s; } .main > *:nth-child(2) { animation-delay: 0.12s; } .main > *:nth-child(3) { animation-delay: 0.19s; } .main > *:nth-child(4) { animation-delay: 0.26s; } .main > *:nth-child(5) { animation-delay: 0.33s; } .main > *:nth-child(6) { animation-delay: 0.40s; } .main > *:nth-child(7) { animation-delay: 0.47s; } .main.hidden { display: none; } .hero-section { text-align: center; max-width: 1000px; width: 100%; margin: 0 auto; padding: var(--spacing-xxl) var(--spacing-md) var(--spacing-xl) var(--spacing-md); overflow: hidden; } .lmn-center { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; margin-top: 4rem; padding-top: 80px; } /* Typography */ .main-heading { font-size: clamp(1.75rem, 6vw, 4rem); font-weight: 400; color: #1a1a1a; line-height: 1.1; margin: var(--spacing-sm) 0 var(--spacing-xxl) 0; letter-spacing: -0.03em; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); animation: fadeInUp 0.8s ease-out; max-width: 1000px; width: 100%; word-wrap: break-word; overflow-wrap: break-word; } .title { font-size: var(--font-size-xxl); font-weight: 700; margin-bottom: var(--spacing-md); letter-spacing: 0.01rem; text-align: left; color: var(--text-primary); } .subtitle { font-size: clamp(0.95rem, 2.5vw, var(--font-size-lg)); font-weight: 500; font-family: var(--font-family); color: var(--text-secondary); margin-bottom: var(--spacing-xl); margin-left: auto; margin-right: auto; text-align: center; max-width: 700px; width: 100%; line-height: 1.6; animation: fadeInUp 0.8s ease-out 0.2s both; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); word-wrap: break-word; overflow-wrap: break-word; } .lmn-install-title { font-size: var(--font-size-xxxl); font-weight: 400; color: var(--text-primary); letter-spacing: 0.01em; margin: 0.8em 0 0.3em 0; font-family: inherit; text-align: center; display: block; line-height: 1.05; margin: var(--spacing-md) 0 var(--spacing-md) 0; letter-spacing: -0.03em; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); animation: fadeInUp 0.8s ease-out; max-width: 1000px; width: 100%; } /* === Animations === */ @keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.8; } } /* === Button Components === */ .btn-base { display: inline-block; font-weight: 700; border: none; border-radius: var(--radius-xl); cursor: pointer; transition: var(--transition-medium); text-decoration: none; outline: none; letter-spacing: 0.01em; position: relative; overflow: hidden; } .btn-base::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); transition: left 0.5s ease; } .btn-base:hover::before { left: 100%; } .download-btn, .dev-btn, .marketplace-cta-btn { display: inline-block; font-weight: 700; border: none; border-radius: var(--radius-xl); cursor: pointer; transition: var(--transition-medium); text-decoration: none; outline: none; letter-spacing: 0.01em; position: relative; overflow: hidden; font-size: 1.1rem; padding: var(--spacing-md) 2.5rem 0.8rem 2.5rem; margin-bottom: var(--spacing-md); display: flex; flex-direction: column; align-items: center; line-height: 1.2; min-width: min(250px, 80vw); max-width: min(250px, 80vw); height: 70px; justify-content: center; } .download-btn { background: linear-gradient(135deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 50%, var(--primary-yellow-darker) 100%); color: var(--text-primary); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1), 0 2px 0 rgba(255, 255, 255, 0.3) inset; } .download-btn:hover, .download-btn:focus { box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15), 0 2px 0 rgba(255, 255, 255, 0.4) inset; transform: translateY(-2px) scale(1.02); } .download-btn:active { background: var(--primary-yellow); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); transform: scale(0.98); } .dev-btn, .marketplace-cta-btn { background: linear-gradient(135deg, var(--bg-white) 0%, #f8f9fa 100%); color: #333; box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15), 0 2px 0 rgba(255, 255, 255, 0.6) inset; } .dev-btn:hover, .dev-btn:focus { background: linear-gradient(135deg, #f8f9fa 0%, var(--bg-white) 100%); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15), 0 2px 0 rgba(255, 255, 255, 0.6) inset; transform: translateY(-2px) scale(1.02); } .marketplace-cta-btn:hover, .marketplace-cta-btn:focus { transform: none; } .dev-btn:active { background: #ededed; box-shadow: 0 2px 8px rgba(187, 187, 187, 0.13); border-color: #888; transform: scale(0.98); } /* Marketplace button - single line variant */ .marketplace-cta-btn { display: inline-block; min-width: auto; max-width: none; height: auto; padding: var(--spacing-md) 2rem; font-size: 1rem; flex-direction: unset; align-items: unset; justify-content: unset; } .download-btn .download-sub, .dev-btn .dev-sub { font-size: var(--font-size-sm); color: #3d4000; font-weight: 400; opacity: 0.7; margin-top: 0.25em; letter-spacing: 0.01em; font-style: italic; text-decoration: none; } .button-row { display: flex; flex-direction: row; justify-content: center; align-items: center; gap: var(--spacing-lg); margin-bottom: var(--spacing-sm); animation: fadeInUp 0.8s ease-out 0.2s both; } .lmn-btn { display: inline-block; background: linear-gradient(90deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 100%); color: var(--text-primary) !important; font-weight: 700; font-size: 1.18rem; border: none; border-radius: var(--radius-xl); padding: 1.1em 2.5em 0.9em 2.5em; margin: 0.2em 0.2em 0.2em 0; text-decoration: none !important; box-shadow: var(--shadow-light), 0 1.5px 0 var(--bg-primary) inset; transition: var(--transition-fast); cursor: pointer; outline: none; letter-spacing: 0.01em; min-width: 150px; } .lmn-btn:hover, .lmn-btn:focus { box-shadow: var(--shadow-medium), 0 1.5px 0 var(--bg-primary) inset; transform: translateY(-1px) scale(1.01); } .lmn-btn:active { background: var(--primary-yellow); box-shadow: var(--shadow-light); transform: scale(0.98); } .lmn-btn:visited { color: var(--text-primary); } /* === Community Section === */ .community-section { display: flex; flex-direction: column; justify-content: center; align-items: center; gap: var(--spacing-lg); margin: 0 auto; max-width: 1000px; width: 100%; padding: var(--spacing-sm) var(--spacing-md) 2rem var(--spacing-md); animation: fadeInUp 0.8s ease-out 0.5s both; position: relative; overflow: hidden; } .community-section .subtitle { margin-bottom: var(--spacing-sm); text-align: center; } .community-buttons { display: flex; justify-content: center; align-items: center; gap: var(--spacing-lg); flex-wrap: wrap; } .community-buttons a { display: inline-block; transition: var(--transition-medium); opacity: 0.9; } .community-buttons a:hover { transform: translateY(-2px); opacity: 1; } .community-buttons img { height: 32px; transition: var(--transition-medium); border-radius: var(--radius-sm); } /* === Content Sections === */ .partner-apps { display: flex; flex-direction: column; justify-content: center; align-items: center; gap: var(--spacing-lg); margin: 0 auto; padding: var(--spacing-xl) 0; max-width: 1000px; width: 100%; padding: var(--spacing-lg) var(--spacing-lg); /* background: transparent; */ animation: fadeInUp 0.8s ease-out 0.6s both; position: relative; } .partner-apps .subtitle { margin-bottom: var(--spacing-sm); text-align: center; } .partner-apps-images { display: flex; justify-content: center; align-items: center; gap: clamp(1rem, 4vw, 2.5rem); flex-wrap: wrap; max-width: 100%; padding: 0 var(--spacing-sm); } .partner-apps a { display: inline-block; transition: var(--transition-slow); opacity: 0.85; position: relative; } .partner-apps a:hover::before { width: 120px; height: 120px; } .partner-apps a:hover:not(.marketplace-cta-btn) { transform: translateY(-6px) scale(1.05); opacity: 1; box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2); } .partner-apps img { width: 56px; height: 56px; object-fit: contain; border-radius: var(--radius-md); transition: var(--transition-medium); } .partner-apps a:hover img { transform: scale(1.05); } .partner-apps-link { margin-top: var(--spacing-sm); text-align: center; } .view-all-link { display: inline-block; color: var(--accent-gold); font-size: 0.95rem; font-weight: 600; text-decoration: none; padding: 10px 20px; border: 2px solid var(--accent-gold); border-radius: var(--radius-lg); transition: var(--transition-medium); } .view-all-link:hover { background: var(--accent-gold); color: var(--text-primary); transform: translateY(-2px); } /* Video Section */ .video-section { margin: var(--spacing-xxl) auto var(--spacing-xl) auto; max-width: 1000px; width: 100%; padding: 0 var(--spacing-md); position: relative; } .video-container { position: relative; width: 100%; height: 0; padding-bottom: 56.25%; /* 16:9 aspect ratio */ background: linear-gradient(45deg, #000 0%, #1a1a1a 100%); border-radius: var(--radius-xxl); overflow: hidden; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3), 0 8px 24px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1); transition: var(--transition-slow); border: 1px solid rgba(255, 255, 255, 0.1); } .video-container:hover { transform: translateY(-8px) scale(1.02); box-shadow: 0 30px 80px rgba(0, 0, 0, 0.4), 0 12px 32px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2); } .video-container iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; border-radius: var(--radius-xxl); } /* Features Section */ .features-section { margin: var(--spacing-xl) auto; max-width: 1100px; width: 100%; padding: var(--spacing-xl); background: rgba(255, 255, 255, 0.6); border-radius: 24px; border: 1px solid rgba(255, 255, 255, 0.2); box-shadow: var(--shadow-heavy); backdrop-filter: blur(8px); overflow: hidden; } .features-grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, 1fr); gap: var(--spacing-md); margin: 0 auto; } .feature-box { background: transparent; border-radius: 12px; padding: var(--spacing-md); text-align: center; border: 1px solid rgba(0, 0, 0, 0.06); display: flex; flex-direction: column; align-items: center; justify-content: flex-start; transition: var(--transition-medium); } .feature-box:hover { background: rgba(255, 255, 255, 0.5); position: relative; } .feature-icon { width: 56px; height: 56px; margin-bottom: var(--spacing-md); display: flex; align-items: center; justify-content: center; } .feature-icon img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; } .feature-title { font-size: var(--font-size-base); font-weight: 600; color: var(--text-primary); margin-bottom: var(--spacing-sm); line-height: 1.3; min-height: 2.6em; /* Reserve space for 2 lines */ display: flex; align-items: center; justify-content: center; } .feature-subtitle { font-size: var(--font-size-sm); color: var(--text-secondary); line-height: 1.5; margin: 0; } /* Responsive design for features */ @media (max-width: 1024px) { .features-section { margin: var(--spacing-xl) var(--spacing-md); padding: var(--spacing-lg); } .features-grid { grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(4, 1fr); gap: var(--spacing-md); } } @media (max-width: 768px) { .features-section { padding: var(--spacing-md); } .features-grid { grid-template-columns: 1fr; grid-template-rows: repeat(8, 1fr); gap: var(--spacing-sm); } .feature-box { padding: var(--spacing-md); } } /* Release Announcement */ .release-announcement { margin: var(--spacing-xl) auto; max-width: 1000px; background: rgba(255, 255, 255, 0.6); border-radius: 20px; padding: 0 var(--spacing-md); box-shadow: var(--shadow-heavy); backdrop-filter: blur(8px); border: 1px solid rgba(255, 255, 255, 0.2); animation: fadeInUp 0.8s ease-out 0.8s both; position: relative; overflow: hidden; display: flex; flex-direction: column; } .release-announcement::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(45deg, transparent 30%, rgba(255, 224, 102, 0.05) 50%, transparent 70%); border-radius: 20px; pointer-events: none; } .release-content { display: flex; align-items: flex-start; gap: var(--spacing-lg); flex-grow: 1; margin-bottom: var(--spacing-lg); padding: var(--spacing-md) var(--spacing-md) 1rem var(--spacing-md); } .release-icon { font-size: var(--spacing-xl); flex-shrink: 0; } .release-info { flex-grow: 1; text-align: left; } .release-title { font-size: 1.1rem; font-weight: 600; color: #000000; margin: 0 0 var(--spacing-sm) 0; text-transform: uppercase; letter-spacing: 0.5px; } .release-name { font-size: 1.3rem; font-weight: 700; color: #333; margin: 0 0 0.3rem 0; line-height: 1.2; } .release-date { font-size: 0.9rem; color: var(--text-muted); margin: 0 0 0.8rem 0; font-weight: 500; } .release-highlights { margin-top: var(--spacing-sm); } .highlight-item { font-size: 0.9rem; color: var(--text-secondary); margin: 0.4rem 0; padding: 0.3rem 0.8rem; border-left: 3px solid var(--accent-gold); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; line-height: 1.3; } .release-link { background: linear-gradient(135deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 50%, var(--primary-yellow-darker) 100%); color: var(--text-primary); text-decoration: none; padding: var(--spacing-md) 2rem; border-radius: var(--radius-xl); font-weight: 700; font-size: var(--font-size-base); padding: 0.8rem var(--spacing-lg); transition: var(--transition-medium); box-shadow: 0 6px 20px rgba(255, 224, 102, 0.4), 0 2px 0 rgba(255, 255, 255, 0.3) inset; align-self: flex-start; margin: auto var(--spacing-md) var(--spacing-md) var(--spacing-md); display: inline-block; cursor: pointer; outline: none; letter-spacing: 0.01em; position: relative; overflow: hidden; min-width: 140px; text-align: center; } .release-link:hover { transform: translateY(-2px) scale(1.02); box-shadow: 0 8px 25px rgba(255, 224, 102, 0.5), 0 2px 0 rgba(255, 255, 255, 0.4) inset; } .release-link:active { background: var(--primary-yellow); box-shadow: 0 2px 8px rgba(255, 224, 102, 0.33); transform: scale(0.98); } /* News Banner */ .news-ticker-outer { min-height: 40px; display: flex; align-items: center; overflow: hidden; width: 100%; } .news-ticker-track { display: flex; align-items: center; white-space: nowrap; will-change: transform; animation: ticker-scroll 30s linear infinite; } .news-ticker-outer:hover .news-ticker-track { animation-play-state: paused; } .news-banner { max-width: 1000px; width: 100%; margin: var(--spacing-md) auto; overflow: hidden; min-height: 48px; padding: 1.2rem 2rem; } .news-banner-content { display: flex; align-items: center; gap: var(--spacing-md); overflow: hidden; position: relative; } .news-label { font-size: var(--font-size-sm); font-weight: 700; color: var(--accent-gold-dark); letter-spacing: 0.05em; display: flex; align-items: center; gap: 0.5em; } .news-item { flex-grow: 1; min-width: 0; overflow: hidden; position: relative; height: 40px; display: flex; align-items: center; } .news-ticker-container { display: flex; align-items: center; white-space: nowrap; will-change: transform; animation: scroll-banner 40s linear infinite; position: absolute; top: 0; left: 0; height: 40px; } .news-ticker-item, .news-title { font-size: var(--font-size-base); font-weight: 600; color: var(--text-primary); text-decoration: none; white-space: nowrap; transition: var(--transition-medium); display: inline-flex; align-items: center; height: 40px; } .news-ticker-item { margin-right: 8px; margin-left: 8px; } .news-ticker-item:hover, .news-title:hover { color: var(--accent-gold-dark); text-decoration: underline; } .news-ticker-separator { font-size: var(--font-size-base); color: var(--text-primary); margin: 0 8px; display: inline-flex; align-items: center; height: 40px; } .news-ticker-container:hover { animation-play-state: paused; } /* Keyframes */ @keyframes ticker-scroll { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } } @keyframes scroll-banner { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } } @keyframes scroll-horizontal { 0% { left: 100%; } 100% { left: -100%; } } /* Logo Ticker */ .logo-ticker-outer { width: 100%; max-width: 100vw; overflow: hidden; margin: var(--spacing-sm) 0; } .logo-ticker-track { display: flex; align-items: center; gap: 2.5rem; white-space: nowrap; will-change: transform; } .logo-ticker-track a { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; height: clamp(40px, 10vw, 60px); } .logo-ticker-track img { height: clamp(40px, 10vw, 60px); width: auto; object-fit: contain; } /* Responsive Design */ @media (max-width: 600px) { .news-ticker-container { animation-duration: 25s; } } @media (max-width: 768px) { .news-banner-content { flex-direction: column; align-items: flex-start; gap: 0.5em; padding: 4rem 1rem; } .news-label { margin-bottom: 0.3em; } .news-ticker-container { animation: scroll-horizontal 15s linear infinite; } .news-title:not(.news-ticker) { animation: none; white-space: normal; overflow: visible; position: static; } } /* === Installer Components === */ .lmn-installer-table { border-collapse: separate; border-spacing: 0; margin: 0.5em 0; font-family: inherit; color: var(--text-primary); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); border-radius: var(--radius-lg); overflow: hidden; background: var(--bg-white); table-layout: fixed; width: 100%; max-width: 650px; position: relative; } .lmn-installer-table::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(45deg, transparent 30%, rgba(255, 224, 102, 0.02) 50%, transparent 70%); pointer-events: none; border-radius: var(--radius-lg); } .lmn-installer-table td, .lmn-installer-table th { border: 1px solid rgba(255, 224, 102, 0.4); text-align: center; font-size: var(--font-size-base); background: var(--bg-white); min-width: 120px; height: 60px; vertical-align: middle; transition: var(--transition-medium); cursor: pointer; font-weight: 500; position: relative; overflow: hidden; line-height: 1.4; } .lmn-installer-table td:hover:not(.lmn-label):not(.lmn-active) { background: rgba(255, 224, 102, 0.1); transform: translateY(-1px); box-shadow: 0 2px 8px rgba(255, 224, 102, 0.2); } .lmn-installer-table tr:first-child td:first-child, .lmn-installer-table tr:first-child th:first-child { border-top-left-radius: var(--radius-lg); } .lmn-installer-table tr:first-child td:last-child, .lmn-installer-table tr:first-child th:last-child { border-top-right-radius: var(--radius-lg); } .lmn-installer-table tr:last-child td:first-child, .lmn-installer-table tr:last-child th:first-child { border-bottom-left-radius: var(--radius-lg); } .lmn-installer-table tr:last-child td:last-child, .lmn-installer-table tr:last-child th:last-child { border-bottom-right-radius: var(--radius-lg); } .lmn-badge { display: inline-block; background: #007acc; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; margin-right: 8px; margin-bottom: 4px; font-weight: 500; } .lmn-badges { display: flex; flex-wrap: wrap; gap: 0.5em; justify-content: flex-start; margin-bottom: 1.2em; } /* Ensure active state is properly defined */ .lmn-installer-table .lmn-active { background: linear-gradient(135deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 100%) !important; font-weight: 600 !important; color: var(--text-primary) !important; box-shadow: 0 2px 8px rgba(255, 224, 102, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.3) !important; border-color: var(--primary-yellow-darker) !important; } /* Ensure disabled state works */ .lmn-installer-table .lmn-disabled { background: #f5f5f5 !important; color: #616161 !important; opacity: 0.6; } /* Label cells styling */ .lmn-installer-table .lmn-label { background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%) !important; font-weight: 600; color: var(--text-primary); cursor: default !important; border-right: 3px solid var(--primary-yellow-darker) !important; text-transform: uppercase; font-size: 0.9rem; letter-spacing: 0.5px; } /* Copy button improvements */ .lmn-copy-btn { display: none; position: absolute; right: 0.7em; top: 50%; transform: translateY(-50%); background: var(--primary-yellow); border: none; border-radius: 6px; color: var(--text-primary); font-size: 0.9rem; padding: 0.3em 0.6em; cursor: pointer; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); transition: var(--transition-fast); z-index: 10; font-weight: 500; } .lmn-command-line:hover .lmn-copy-btn { display: inline-block; } .lmn-copy-btn:hover { background: var(--primary-yellow-dark); transform: translateY(-50%) scale(1.05); } /* Content section improvements */ .lmn-content-section { background: var(--bg-white); border-radius: var(--radius-lg); padding: var(--spacing-lg); margin-top: var(--spacing-lg); box-shadow: var(--shadow-light); max-width: 650px; width: 100%; box-sizing: border-box; } /* Download section styling */ .lmn-download-section { border-radius: var(--radius-lg); padding: 0.5em 0 1em 0; margin-bottom: 0.8em; text-align: center; } .lmn-download-section a { color: var(--text-primary); text-decoration: none; font-weight: 600; font-size: 1.1rem; display: inline-block; padding: var(--spacing-md) var(--spacing-xl); background: linear-gradient(135deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 100%); border-radius: var(--radius-md); transition: var(--transition-medium); border: 1px solid rgba(230, 184, 0, 0.3); box-shadow: 0 4px 16px rgba(255, 224, 102, 0.4); min-width: 250px; } .lmn-download-section a:hover { background: linear-gradient(135deg, var(--primary-yellow-dark) 0%, var(--primary-yellow-darker) 100%); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(255, 224, 102, 0.5); } .lmn-command-area { background: var(--bg-primary); border: none; border-radius: var(--radius-xl); margin-top: 0.8em; padding: 0.8em var(--spacing-md); box-shadow: none; max-width: 600px; width: 100%; font-family: inherit; display: flex; flex-direction: column; align-items: center; } .lmn-command-area b { font-size: 1.13rem; color: var(--text-primary); font-weight: 600; margin: 0 0 1em 0; padding-bottom: 0.5em; border-bottom: 2px solid #ffe066; display: flex; align-items: center; gap: 0.5em; } .lmn-section-header::before { content: "โšก"; font-size: 1.1em; } .lmn-explore-header::before { content: "๐Ÿš€"; } .lmn-badges { display: flex; flex-wrap: wrap; gap: 0.5em; justify-content: flex-start; margin-bottom: 1.2em; } .lmn-badges img { border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.2s ease; } .lmn-badges img:hover { transform: translateY(-1px); } /* Command styling improvements */ .lmn-command pre { background: var(--text-primary); color: var(--bg-white); border-radius: 10px; padding: 0.8em 4.5em 0.8em var(--spacing-md); font-size: 1.08rem; margin: 0.5em 0 0 0; font-family: 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', 'monospace'; overflow-x: auto; box-shadow: 0 2px 8px rgba(255, 224, 102, 0.2); width: 100%; min-width: 0; max-width: 100%; position: relative; } .lmn-download-section { border-radius: 12px; padding: 0.5em 0 1em 0; margin-bottom: 0.8em; text-align: center; } .lmn-download-section a { color: #222; text-decoration: none; font-weight: 600; font-size: 1.15rem; display: inline-block; padding: 1em 2em; background: linear-gradient(135deg, #ffe066 0%, #ffd43b 100%); border-radius: 10px; transition: all 0.2s ease; border: 1px solid rgba(230, 184, 0, 0.3); box-shadow: 0 4px 16px rgba(255, 224, 102, 0.4); min-width: 280px; line-height: 1.3; } .lmn-download-section a:hover { background: linear-gradient(135deg, #ffd43b 0%, #ffcc00 100%); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(255, 224, 102, 0.5); } .lmn-command code { background: none; color: inherit; font-size: inherit; font-family: inherit; padding: 0; display: block; width: 100%; box-sizing: border-box; } .lmn-command a { color: var(--accent-gold); text-decoration: underline; font-size: 1.08rem; } .lmn-command a:hover { color: var(--accent-gold-dark); } .lmn-command-line { display: flex; align-items: center; position: relative; padding-right: 0; min-width: 0; word-break: break-all; width: calc(100% + 5.7em); margin-left: -1.2em; padding-left: 1.2em; } .lmn-command-line span { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; padding-right: 0.5em; max-width: calc(100% - 5.7em); } .lmn-copy-btn { display: none; position: absolute; right: 0.7em; top: 50%; transform: translateY(-50%); background: var(--primary-yellow); border: none; border-radius: 0.7em; color: var(--text-primary); font-size: 1.02rem; padding: 0.2em 0.8em; cursor: pointer; box-shadow: 0 2px 8px rgba(255, 224, 102, 0.2); transition: var(--transition-fast); z-index: 2; flex-shrink: 0; } .lmn-command-line:hover .lmn-copy-btn { display: inline-block; } .lmn-copy-btn:hover { transform: translateY(-50%) scale(1.05); } .lmn-copy-btn:active { background: var(--primary-yellow-dark); } .lmn-install-view { display: none; flex-direction: column; align-items: center; justify-content: flex-start; width: 100%; margin-top: var(--spacing-md); padding: 0 var(--spacing-xl); } .lmn-installer-container { margin-top: 0.8em; width: 100%; display: flex; flex-direction: column; align-items: center; } /* === Footer === */ .site-footer { width: 100%; padding: calc(var(--spacing-xxl) / 8) 0 var(--spacing-lg) 0; margin: calc(var(--spacing-xxl) / 8) auto 0 auto; background-color: transparent; border-top: 1px solid #f3f4f6; position: relative; overflow: hidden; } .footer-content { max-width: 1160px; margin: 0 auto; padding: 0 var(--spacing-lg); display: block; margin-bottom: var(--spacing-md); } .footer-nav-rail { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.9rem 1.15rem; padding: 0.1rem 0 0.85rem; border-bottom: 1px solid rgba(0, 0, 0, 0.09); } .footer-rail-link { text-decoration: none; display: inline-flex; align-items: center; justify-content: center; gap: 0.44rem; min-height: 24px; padding: 0.2rem 0; position: relative; color: #3a4251; font-size: 0.95rem; font-weight: 650; letter-spacing: 0.005em; transition: color 0.2s ease; } .footer-rail-link::after { content: ""; position: absolute; left: 0; right: 0; bottom: -0.26rem; height: 2px; border-radius: 999px; background: currentColor; opacity: 0; transform: scaleX(0.35); transform-origin: center; transition: transform 0.2s ease, opacity 0.2s ease; } .footer-rail-link:hover::after, .footer-rail-link:focus::after { opacity: 0.42; transform: scaleX(1); } .footer-rail-link--community { color: #1f2438; } .footer-rail-link--community:hover, .footer-rail-link--community:focus { color: #111827; } .footer-rail-link--product { color: #1f2438; } .footer-rail-link--product:hover, .footer-rail-link--product:focus { color: #111827; } .footer-rail-icon { width: 15px; height: 15px; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; line-height: 0; } .footer-rail-icon img { width: 100%; height: 100%; object-fit: contain; } .footer-meta { margin-top: 0.5rem; display: flex; justify-content: center; } .copyright { font-size: 0.95rem; color: #6b7280; margin: 0; font-weight: 400; } /* Footer responsive design */ @media (min-width: 1024px) { } @media (max-width: 1023px) { .footer-content { padding: 0 var(--spacing-md); } } @media (max-width: 768px) { .site-footer { padding: var(--spacing-xl) 0 var(--spacing-lg) 0; } .footer-content { padding: 0 var(--spacing-md); } .footer-nav-rail { gap: 0.72rem 0.9rem; padding-bottom: 0.72rem; } } @media (max-width: 640px) { .footer-content { padding: 0 var(--spacing-lg); } } @media (max-width: 480px) { .site-footer { padding: var(--spacing-lg) 0; } .footer-content { padding: 0 var(--spacing-md); } .footer-nav-rail { gap: 0.6rem 0.72rem; } .footer-rail-link { font-size: 0.92rem; } .copyright { font-size: 0.85rem; } } /* === News Components === */ .news-sidebar { position: fixed; top: 0; left: 0; width: 250px; height: 100vh; background: var(--bg-primary); border-right: 2px solid var(--border-light); box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); z-index: 150; padding: var(--spacing-xl) var(--spacing-lg); overflow-y: auto; transition: var(--transition-medium); } .news-sidebar.collapsed { transform: translateX(-250px); } .sidebar-header { margin-bottom: var(--spacing-xl); } .sidebar-header h3 { font-size: var(--font-size-lg); font-weight: 600; color: #333; margin-bottom: var(--spacing-sm); margin-top: 2.5rem; } .sidebar-header p { font-size: 0.9rem; color: var(--text-muted); margin: 0; } .filter-container { display: flex; flex-direction: column; gap: 0.8rem; margin-top: 5rem; /* Prevents filter buttons from sitting behind navbar */ margin-bottom: var(--spacing-xl); } .filter-btn { background: var(--bg-white); border: 2px solid var(--border-light); border-radius: var(--radius-md); padding: 0.8rem 1.2rem; font-size: 0.95rem; font-weight: 500; color: #333; cursor: pointer; transition: var(--transition-medium); display: flex; align-items: center; gap: var(--spacing-sm); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); text-align: left; width: 100%; } .filter-btn:hover { background: #f9f9f9; border-color: var(--border-medium); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); } .filter-btn.active { background: linear-gradient(90deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 100%); border-color: var(--accent-gold); color: var(--text-primary); font-weight: 600; box-shadow: var(--shadow-medium); } .filter-btn.active:hover { transform: translateY(-1px); box-shadow: 0 6px 16px rgba(230, 184, 0, 0.4); } .sort-container { display: flex; flex-direction: column; gap: var(--spacing-sm); margin-bottom: var(--spacing-xl); } .sort-label { font-size: 0.9rem; font-weight: 600; color: #333; } .sort-select { background: var(--bg-white); border: 2px solid var(--border-light); border-radius: var(--radius-md); padding: 0.6rem 0.8rem; font-size: 0.9rem; color: #333; cursor: pointer; transition: var(--transition-medium); outline: none; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); width: 100%; } .sort-select:hover { border-color: var(--border-medium); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); } .sort-select:focus { border-color: var(--accent-gold); box-shadow: var(--shadow-medium); } .news-view { padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-xl) 270px; max-width: 1200px; margin: 0 auto; flex-grow: 1; transition: var(--transition-medium); } .news-view.sidebar-collapsed { padding-left: 50px; } .news-view .hero-section { text-align: center; max-width: 1000px; margin: 0 auto var(--spacing-xl) auto; padding: var(--spacing-md); } .news-view .main-heading { font-size: 3.5rem; font-weight: 400; color: #1a1a1a; line-height: 1.05; margin: var(--spacing-sm) 0 var(--spacing-md) 0; letter-spacing: -0.03em; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); animation: fadeInUp 0.8s ease-out; max-width: 1000px; width: 100%; } .news-view .subtitle { font-size: var(--font-size-lg); font-weight: 500; font-family: var(--font-family); color: var(--text-secondary); margin-bottom: var(--spacing-xxl); text-align: center; max-width: 700px; margin-left: auto; margin-right: auto; line-height: 1.6; animation: fadeInUp 0.8s ease-out 0.2s both; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .news-posts { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--spacing-xl); margin-bottom: var(--spacing-xl); } .news-tile { background: var(--bg-white); border-radius: var(--radius-lg); overflow: hidden; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); transition: var(--transition-medium); aspect-ratio: 0.8; display: flex; flex-direction: column; position: relative; width: 100%; } .news-tile:hover { transform: translateY(-4px); box-shadow: var(--shadow-heavy); } .news-image { height: 40%; background: linear-gradient(135deg, #f4d03f 0%, #f7dc6f 100%); display: flex; align-items: center; justify-content: center; position: relative; } .news-image-placeholder { font-size: var(--spacing-xxl); opacity: 0.8; } .news-image-actual { width: 120%; height: 120%; object-fit: cover; object-position: center; transform: scale(1.1); } .news-content { padding: var(--spacing-lg); flex-grow: 1; display: flex; flex-direction: column; } .news-content h3 { margin: 0 0 var(--spacing-md) 0; color: #333; font-size: var(--font-size-lg); line-height: 1.3; font-weight: 600; } .news-date { color: var(--text-muted); font-size: 0.9rem; font-weight: 500; margin-bottom: 10px; } .news-content p { color: var(--text-secondary); font-size: 0.9rem; line-height: 1.4; margin: 0; flex-grow: 1; display: block; overflow: visible; text-overflow: unset; } .news-tile-link { text-decoration: none; color: inherit; display: flex; flex-direction: column; height: 100%; } .news-tile-link:hover { text-decoration: none; color: inherit; } .youtube-play-button { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.8); color: white; border-radius: var(--radius-round); width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; font-size: 20px; transition: var(--transition-medium); pointer-events: none; } .news-tile:hover .youtube-play-button { background: rgba(255, 0, 0, 0.9); transform: translate(-50%, -50%) scale(1.1); } .content-type-badge { position: absolute; bottom: 12px; right: 12px; padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: 500; color: white; z-index: 10; } .blog-badge { background-color: #007acc; } .video-badge { background-color: #dc3545; } .tutorial-badge { background-color: #28a745; } .release-badge { background-color: var(--primary-yellow); } /* News sidebar controls */ .collapse-btn, .expand-btn { background: var(--bg-white); border: 2px solid var(--border-light); border-radius: var(--radius-sm); padding: 0.4rem 0.6rem; font-size: 0.9rem; color: #333; cursor: pointer; transition: var(--transition-medium); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); z-index: 10; } .collapse-btn { position: absolute; top: var(--spacing-md); right: var(--spacing-md); } .expand-btn { position: fixed; top: 4.5rem; left: 1.5rem; background: linear-gradient(90deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 100%); border: none; font-weight: 500; color: var(--text-primary); box-shadow: var(--shadow-light); z-index: 160; opacity: 0; visibility: hidden; transform: translateX(-20px); } .expand-btn.show { opacity: 1; visibility: visible; transform: translateX(0); } .expand-btn:hover { box-shadow: var(--shadow-medium); transform: translateY(-1px); } .sidebar-toggle { display: none; } .sidebar-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 140; opacity: 0; visibility: hidden; transition: var(--transition-medium); } .sidebar-overlay.active { opacity: 1; visibility: visible; } /* === Utility Classes === */ .back-to-top-btn { position: fixed; bottom: 80px; right: 20px; width: 50px; height: 50px; background: linear-gradient(90deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 100%); border: none; border-radius: var(--radius-round); box-shadow: var(--shadow-medium); cursor: pointer; opacity: 0; visibility: hidden; transform: translateY(20px); transition: var(--transition-medium); z-index: 1000; display: flex; align-items: center; justify-content: center; } .back-to-top-btn.show { opacity: 1; visibility: visible; transform: translateY(0); } .back-to-top-btn:hover { box-shadow: 0 6px 20px rgba(230, 184, 0, 0.4); transform: translateY(-2px); } .back-to-top-btn:active { transform: translateY(0); box-shadow: var(--shadow-light); } .back-to-top-icon { font-size: 20px; font-weight: bold; color: var(--text-primary); line-height: 1; } .text-muted { color: var(--border-medium); } .command-margin { margin-top: 0.6em; } /* === Install Selector Components === */ /* Unified content container */ .lmn-content-section { background: #fff; border-radius: 18px; padding: 1.5em; margin-top: 1.2em; box-shadow: 0 4px 24px #ffe06633; max-width: 650px; width: 100%; box-sizing: border-box; } .lmn-section-header { font-size: 1.2rem; color: #222; font-weight: 600; margin: 0 0 1em 0; padding-bottom: 0.5em; border-bottom: 2px solid #ffe066; display: flex; align-items: center; gap: 0.5em; } .lmn-section-header::before { content: "โšก"; font-size: 1.1em; } .lmn-install-section-title { font-size: 1.2rem; color: #333; font-weight: 700; margin: 2em 0 1em 0; padding-bottom: 0.5em; border-bottom: 2px solid #e0e0e0; } .lmn-install-section-title:first-child { margin-top: 0; } .lmn-install-method-header { font-size: 1.05rem; color: #555; font-weight: 600; margin: 1.5em 0 0.8em 0; } .lmn-install-method-header:first-child { margin-top: 0; } .lmn-explore-header::before { content: "๐Ÿš€"; } .lmn-badges { display: flex; flex-wrap: wrap; gap: 0.5em; justify-content: flex-start; margin-bottom: 1.2em; } .lmn-badges img { border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.2s ease; } .lmn-badges img:hover { transform: translateY(-1px); } /* Command styling improvements */ .lmn-command pre { background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); color: #fff; border-radius: 12px; padding: 1em 4.5em 1em 1.2em; font-size: 1.06rem; margin: 0; font-family: 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', 'monospace'; overflow-x: auto; box-shadow: 0 4px 16px rgba(0,0,0,0.15); border: 1px solid rgba(255, 224, 102, 0.2); position: relative; } .lmn-download-section { border-radius: 12px; padding: 0.5em 0 1em 0; margin-bottom: 0.8em; text-align: center; } .lmn-download-section a { color: #222; text-decoration: none; font-weight: 600; font-size: 1.15rem; display: inline-block; padding: 1em 2em; background: linear-gradient(135deg, #ffe066 0%, #ffd43b 100%); border-radius: 10px; transition: all 0.2s ease; border: 1px solid rgba(230, 184, 0, 0.3); box-shadow: 0 4px 16px rgba(255, 224, 102, 0.4); min-width: 280px; line-height: 1.3; } .lmn-download-section a:hover { background: linear-gradient(135deg, #ffd43b 0%, #ffcc00 100%); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(255, 224, 102, 0.5); } .lmn-command code { background: none; color: inherit; font-size: inherit; font-family: inherit; padding: 0; display: block; width: 100%; box-sizing: border-box; } .lmn-command a { color: #e6b800; text-decoration: underline; font-size: 1.08rem; } .lmn-command a:hover { color: #bfa100; } .lmn-note { margin-top: 0.8em; color: #666; font-size: 1.02rem; line-height: 1.5; } .lmn-note a { color: #666; text-decoration: underline; } .lmn-note a:hover { color: #444; } .lmn-note code { font-family: 'Fira Mono', 'Consolas', 'Menlo', 'Monaco', monospace; background: #f5f5f5; padding: 2px 6px; border-radius: 4px; font-size: 0.95em; } .lmn-source-note { margin-top: 0.8em; } .lmn-coming-soon { text-align: center; font-size: 1.3rem; font-weight: 600; color: #666; padding: 2em 1em; background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%); border-radius: 12px; border: 1px solid #ddd; } .lmn-quickstart-table { margin-top: 1.5em; } .lmn-embedded-table { margin: 0 0 1.2em 0; box-shadow: none; border: 1px solid rgba(255, 224, 102, 0.4); } .lmn-command-line { display: flex; align-items: center; position: relative; padding-right: 0; min-width: 0; word-break: break-all; width: calc(100% + 5.7em); box-sizing: border-box; margin-left: -1.2em; padding-left: 1.2em; } .lmn-command-line span { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; padding-right: 0.5em; max-width: calc(100% - 5.7em); } .lmn-copy-btn { display: none; position: absolute; right: 0.7em; top: 50%; transform: translateY(-50%); background: #ffe066; border: none; border-radius: 0.7em; color: #222; font-size: 1.02rem; padding: 0.2em 0.8em; cursor: pointer; box-shadow: 0 2px 8px rgba(255, 224, 102, 0.4); transition: background 0.18s, color 0.18s, transform 0.18s; z-index: 2; flex-shrink: 0; } .lmn-command-line:hover .lmn-copy-btn { display: inline-block; } .lmn-copy-btn:hover { transform: translateY(-50%) scale(1.05); } .lmn-copy-btn:active { background: #ffd43b; } .lmn-install-view { display: none; flex-direction: column; align-items: center; justify-content: flex-start; width: 100%; margin-top: 1rem; padding: 0 2rem; box-sizing: border-box; } .lmn-installer-container { margin-top: 0.8em; width: 100%; display: flex; flex-direction: column; align-items: center; } /* === Decorative Elements === */ .ai-art { margin: var(--spacing-xxl) auto var(--spacing-lg) auto; display: flex; justify-content: center; align-items: flex-end; min-height: 120px; } /* === Responsive Design === */ @media (max-width: 1024px) { .lmn-install-view { margin-top: var(--spacing-md); } .main { margin-top: 6rem; } .news-posts { grid-template-columns: repeat(auto-fit, minmax(250px, 400px)); gap: var(--spacing-lg); } .news-tile { aspect-ratio: 0.9; } .news-sidebar { width: 220px; padding: var(--spacing-lg) var(--spacing-md); } .news-sidebar.collapsed { transform: translateX(-220px); } .news-view { padding: var(--spacing-lg) var(--spacing-lg) var(--spacing-lg) 240px; } .news-view.sidebar-collapsed { padding-left: 50px; } .back-to-top-btn { bottom: 85px; right: 15px; width: 45px; height: 45px; } .back-to-top-icon { font-size: 18px; } } @media (max-width: 900px) { .news-sidebar { width: 200px; padding: var(--spacing-md) 0.8rem; } .news-sidebar.collapsed { transform: translateX(-200px); } .news-view { padding: var(--spacing-md) var(--spacing-md) var(--spacing-md) 220px; } .news-view.sidebar-collapsed { padding-left: 30px; } .collapse-btn { top: 0.8rem; right: 0.8rem; padding: 0.3rem var(--spacing-sm); font-size: var(--font-size-sm); } .expand-btn { top: 0.8rem; left: 0.8rem; padding: 0.3rem var(--spacing-sm); font-size: var(--font-size-sm); } } @media (max-width: 825px) { .navbar { flex-direction: column; gap: var(--spacing-md); padding: 1rem; } .navbar-links { gap: 1.5rem; font-size: 1.18rem; flex-wrap: wrap; justify-content: center; } .navbar-links a { font-size: 1.18rem; } } @media (max-width: 768px) { .navbar { flex-direction: column; gap: var(--spacing-md); padding: 1rem; } .navbar-links { gap: var(--spacing-lg); font-size: 1.12rem; } .navbar-links a { font-size: 1.12rem; } .navbar-context { position: static; font-size: var(--font-size-sm); order: -1; } .main { margin-top: var(--spacing-xxl); padding: var(--spacing-lg) var(--spacing-sm) 3rem var(--spacing-sm); } .hero-section { padding: var(--spacing-lg) var(--spacing-sm); } .title { font-size: var(--font-size-xxl); } .subtitle { font-size: var(--font-size-base); padding: 0 var(--spacing-sm); } .button-row { flex-direction: column; gap: 1rem; width: 100%; padding: 0 var(--spacing-sm); } .download-btn, .dev-btn { font-size: var(--font-size-base); padding: 0.8rem var(--spacing-lg); min-width: auto; max-width: 100%; width: 100%; } .download-btn .download-sub, .dev-btn .dev-sub { font-size: var(--font-size-sm); } .lmn-install-view { padding: 0 var(--spacing-md); margin-top: var(--spacing-sm); } .lmn-install-title { font-size: var(--spacing-lg); } .lmn-installer-table { font-size: 0.98rem; max-width: 99vw; } .lmn-command-area { max-width: 99vw; } .lmn-btn { font-size: var(--font-size-base); padding: 0.8rem var(--spacing-lg); } .breadcrumb { padding: 0.6rem var(--spacing-md); font-size: var(--font-size-sm); min-height: 50px; } .breadcrumb-separator { margin: 0 var(--spacing-sm); } .lmn-center { padding-top: 70px; } .main-heading { font-size: var(--font-size-xxxl); line-height: 1.2; } .partner-apps { gap: var(--spacing-lg); padding: var(--spacing-xl) var(--spacing-md); } .community-section { padding: var(--spacing-lg) var(--spacing-md); } .community-buttons { flex-direction: column; gap: var(--spacing-md); } .partner-apps img { width: 50px; height: 50px; } .video-section { margin: var(--spacing-xl) auto var(--spacing-xxl) auto; max-width: 100%; } .release-announcement { margin: var(--spacing-xl) var(--spacing-md); max-width: none; } .news-banner { margin: var(--spacing-md) var(--spacing-md); max-width: none; } .news-banner-content { flex-direction: column; gap: var(--spacing-sm); align-items: flex-start; } .news-item { align-items: flex-start; } .news-title { animation: none; /* Disable scrolling on smaller screens */ white-space: normal; overflow: visible; } .release-content { flex-direction: column; gap: var(--spacing-md); margin-bottom: 0; } .release-link { align-self: flex-start; } .back-to-top-btn { bottom: 85px; right: 12px; width: 40px; height: 40px; } .back-to-top-icon { font-size: 16px; } .site-footer { padding: 1.2rem 0; } .footer-content { padding: 0 var(--spacing-md); gap: 0.2rem; } .dad-joke { font-size: var(--font-size-base); } .copyright { font-size: 0.8rem; } /* News responsive styles */ .news-sidebar { transform: translateX(-100%); transition: var(--transition-medium); z-index: 160; width: 250px; padding: var(--spacing-lg); } .news-sidebar.mobile-open { transform: translateX(0); } .news-view { padding: var(--spacing-md); margin-top: var(--spacing-xl); } .news-view.sidebar-collapsed { padding-left: var(--spacing-md); } .news-view .main-heading { font-size: var(--font-size-xxxl); margin: var(--spacing-sm) 0; } .news-view .subtitle { font-size: 1.1rem; margin-bottom: var(--spacing-xl); } .collapse-btn { display: none; } .expand-btn { display: none; } .sidebar-toggle { display: block; position: fixed; top: 80px; right: 20px; z-index: 200; background: linear-gradient(90deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 100%); border: none; border-radius: var(--radius-md); padding: 0.6rem var(--spacing-md); font-size: 0.9rem; font-weight: 500; color: var(--text-primary); cursor: pointer; box-shadow: var(--shadow-light); transition: var(--transition-medium); } .sidebar-toggle:hover { box-shadow: var(--shadow-medium); transform: translateY(-1px); } .news-posts { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--spacing-lg); } .news-header { margin-bottom: var(--spacing-xl); } .news-header h1 { font-size: var(--font-size-xxl); } .news-header p { font-size: var(--font-size-base); } .news-tile { aspect-ratio: 0.9; } .news-content { padding: var(--spacing-md); } .news-content h3 { font-size: var(--font-size-base); line-height: 1.2; } .news-date { font-size: 0.8rem; } .news-content p { font-size: var(--font-size-sm); line-height: 1.3; } .youtube-play-button { width: 45px; height: 45px; font-size: 16px; } } @media (max-width: 600px) { .navbar-links { gap: 1rem; font-size: 1.08rem; flex-wrap: wrap; justify-content: center; } .navbar-links a { font-size: 1.08rem; } .hero-section { padding: var(--spacing-md) var(--spacing-xs); } .main-heading { line-height: 1.15; margin-bottom: var(--spacing-lg); } .button-row { padding: 0; } .release-announcement { margin: var(--spacing-lg) auto; max-width: 95%; padding: var(--spacing-md); } .news-banner { margin: var(--spacing-sm) auto; max-width: 95%; min-height: 40px; } .news-banner-content { flex-direction: column; gap: 0.5rem; align-items: center; text-align: center; } .news-item { align-items: center; } .news-title { animation: none; white-space: normal; overflow: visible; font-size: var(--font-size-sm); } .news-label { font-size: var(--font-size-xs); } .release-content { flex-direction: column; text-align: center; gap: 0.8rem; margin-bottom: 0; } .release-info { text-align: center; } .release-icon { font-size: var(--spacing-lg); } .release-title { font-size: 0.9rem; } .release-name { font-size: var(--font-size-base); } .release-date { font-size: 0.8rem; margin-bottom: var(--spacing-sm); } .highlight-item { font-size: var(--font-size-xs); margin: 0.2rem 0; padding-left: var(--spacing-sm); text-align: left; } .release-link { font-size: var(--font-size-base); padding: 0.8rem var(--spacing-lg); min-width: 120px; align-self: center; } .video-section { margin: var(--spacing-xl) auto var(--spacing-lg) auto; max-width: 95%; } .lmn-install-title { font-size: 1.8rem !important; margin: 0.8em 0 0.4em 0; } .back-to-top-btn { bottom: 80px; right: 10px; width: 35px; height: 35px; } .back-to-top-icon { font-size: 14px; } /* News mobile styles */ .news-sidebar { width: 100%; padding: var(--spacing-md); } .sidebar-toggle { top: 70px; right: 15px; padding: var(--spacing-sm) 0.8rem; font-size: var(--font-size-sm); } .news-view { padding: 0.8rem; margin-top: var(--spacing-lg); } .news-header h1 { font-size: 1.8rem; } .news-header p { font-size: 0.9rem; } .news-posts { grid-template-columns: 1fr; gap: 1.2rem; } .news-tile { aspect-ratio: 1.1; } .news-image { height: 50%; } .news-content { padding: 0.6rem; } .news-content h3 { font-size: 0.9rem; margin-bottom: 0.4rem; line-height: 1.1; } .news-content p { font-size: 0.8rem; line-height: 1.2; margin-bottom: 0.2rem; } .news-date { font-size: var(--font-size-xs); margin-bottom: 0.3rem; } .youtube-play-button { width: 40px; height: 40px; font-size: 14px; } } @media (max-width: 480px) { .navbar { padding: 0.75rem; } .navbar-brand .brand-title { font-size: 1.1rem; } .breadcrumb { padding: var(--spacing-sm) 0.8rem; font-size: 0.8rem; flex-wrap: wrap; min-height: auto; } .breadcrumb-back { font-size: 0.8rem; } .lmn-center { padding-top: 65px; } .lmn-install-title { font-size: var(--spacing-lg) !important; margin: 0.6em 0 0.3em 0; } .main { padding: var(--spacing-md) var(--spacing-xs) 2rem var(--spacing-xs); } .hero-section { padding: var(--spacing-sm) var(--spacing-xs); } .main-heading { margin-bottom: var(--spacing-md); } .download-btn, .dev-btn { font-size: 0.95rem; padding: 0.7rem var(--spacing-md); height: auto; min-height: 60px; } .partner-apps { gap: var(--spacing-md); padding: var(--spacing-lg) var(--spacing-xs); } .partner-apps img { width: 40px; height: 40px; } .community-section { padding: var(--spacing-md) var(--spacing-xs); } .news-view { padding: var(--spacing-sm); margin-top: var(--spacing-md); } .news-view .main-heading { font-size: var(--font-size-xxl); margin: 0.3rem 0; } .news-view .subtitle { font-size: var(--font-size-base); margin-bottom: var(--spacing-lg); } .news-header { margin-bottom: var(--spacing-lg); } .news-header h1 { font-size: 1.6rem; } .news-posts { gap: var(--spacing-md); } .news-tile { aspect-ratio: 1.2; } .news-image { height: 45%; } .news-content { padding: var(--spacing-sm); } .news-content h3 { font-size: 0.8rem; margin-bottom: 0.3rem; line-height: 1.1; } .news-content p { font-size: 0.7rem; line-height: 1.2; margin-bottom: 0.1rem; } .news-date { font-size: 0.65rem; margin-bottom: 0.2rem; } .youtube-play-button { width: 35px; height: 35px; font-size: 12px; } /* Community links responsive styles */ .community-section { padding: var(--spacing-lg) var(--spacing-sm); } .community-buttons { flex-direction: column; gap: var(--spacing-md); } .community-stats { flex-direction: column; gap: var(--spacing-md); } .community-stat-card { min-width: 260px; max-width: 300px; padding: var(--spacing-md); } .stat-icon { width: 35px; height: 35px; font-size: 1.3rem; } .badges-section { flex-direction: column; gap: var(--spacing-md); margin: var(--spacing-xl) 0 var(--spacing-lg) 0; } .badge { min-width: 100px; padding: var(--spacing-xs) var(--spacing-sm); font-size: 0.8rem; } } /* === Blog Article Styles === */ .blog-main { max-width: none; padding-top: 0; } .blog-article { width: 100%; max-width: 1120px; margin: 0 auto 3rem; padding: 1.5rem; } .blog-article .hero-section { background: linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.58)); border: 1px solid rgba(0, 0, 0, 0.06); border-radius: 24px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.05); padding: 3rem 2rem 2.4rem; margin-bottom: 1.5rem; } .hero-content { text-align: center; max-width: 820px; margin: 0 auto; } .hero-badge { display: inline-block; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-gold-dark); background: rgba(255, 224, 102, 0.2); padding: 0.36rem 0.95rem; border-radius: 100px; margin-bottom: 1rem; } .hero-title { font-size: clamp(2rem, 4.2vw, 3.3rem); font-weight: 760; letter-spacing: -0.03em; line-height: 1.08; margin: 0 0 1rem 0; color: var(--text-primary); } .hero-subtitle { margin: 0 auto; max-width: 760px; font-size: clamp(0.96rem, 1.65vw, 1.08rem); line-height: 1.62; color: #505050; } .hero-meta { display: flex; justify-content: center; gap: 0.55rem; flex-wrap: wrap; margin-top: 1.4rem; max-width: 860px; margin-left: auto; margin-right: auto; } .meta-item { font-size: 0.82rem; font-weight: 600; color: #4b4b4b; background: rgba(255, 255, 255, 0.86); border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 999px; padding: 0.38rem 0.8rem; } .meta-item .meta-icon { color: var(--accent-gold-dark); margin-right: 0.32rem; } .meta-item.meta-authors { flex: 1 1 100%; text-align: center; background: transparent; border-color: transparent; color: #636363; padding-top: 0.15rem; } .article-content { max-width: 860px; margin: 0 auto; font-size: var(--font-size-base); line-height: 1.65; color: #2d2d2d; } .section-card { background: rgba(255, 255, 255, 0.48); border: 1px solid rgba(0, 0, 0, 0.05); border-radius: 20px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.025); padding: 1.8rem; margin: 1.35rem 0; } .section-card + .section-card { margin-top: 2.1rem; position: relative; } .section-card + .section-card::before { content: ""; width: 72px; height: 1px; background: rgba(0, 0, 0, 0.1); position: absolute; top: -1.08rem; left: 50%; transform: translateX(-50%); } /* Unified news article layout */ .blog-article-unified .section-card { background: transparent; border: 0; border-radius: 0; box-shadow: none; padding: 1.2rem 0; margin: 0; } .blog-article-unified { margin-top: 1rem; } .blog-article-unified .hero-section { margin-bottom: 2.25rem; } .blog-article-unified .section-header { text-align: left; margin: 0 0 1.15rem; } .blog-article-unified .section-header .section-kicker { margin-bottom: 0.85rem; } .blog-article-unified .section-header .section-title { font-size: clamp(1.65rem, 3.2vw, 2.5rem); font-weight: 750; letter-spacing: -0.03em; line-height: 1.15; margin: 0 0 0.9rem; } .blog-article-unified .section-header p { font-size: clamp(0.96rem, 1.65vw, 1.08rem); color: #505050; margin: 0; max-width: 760px; line-height: 1.62; } .blog-article-unified .section-card + .section-card { margin-top: 2.2rem; padding-top: 2.1rem; } .blog-article-unified .section-card + .section-card::before { content: none; } .blog-article-unified .model-card-enhanced { box-shadow: none; } .blog-article-unified .model-showcase-single { grid-template-columns: minmax(0, 760px); } .blog-article-arcade .model-showcase-single { grid-template-columns: minmax(0, 1fr); } .blog-article-arcade .model-card-enhanced { max-width: 100%; padding: 1.25rem; display: flex; flex-direction: column; gap: 0.6rem; } .blog-article-arcade .model-card-enhanced > p { margin: 0; } .blog-article-arcade .model-card-enhanced .model-title { margin: 0 0 0.35rem; } .blog-article-arcade .model-stat-grid { margin-top: 0.2rem; margin-bottom: 0.15rem; border-top: 0; padding-top: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.6rem; } .blog-article-arcade .model-stat-row { display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; gap: 0.25rem; border: 1px solid rgba(0, 0, 0, 0.08); background: rgba(255, 255, 255, 0.55); border-radius: 10px; padding: 0.6rem 0.7rem; } .blog-article-arcade .model-stat-row strong { color: #2d2d2d; } .blog-article-arcade .arcade-demo-media { width: 50%; margin-left: auto; margin-right: auto; } .blog-article-unified .blog-callout { margin-top: 1.15rem; background: rgba(255, 255, 255, 0.8); border: 1px solid rgba(230, 184, 0, 0.36); border-left: 4px solid var(--accent-gold); border-radius: 12px; padding: 0.95rem 1rem 0.95rem 0.9rem; box-shadow: none; } .blog-article-unified .blog-callout-title { color: #3f3200; } .blog-article-unified .blog-callout p { color: #3f3f3f; } .blog-article-unified .blog-download-table { background: rgba(255, 255, 255, 0.86); border-color: rgba(0, 0, 0, 0.08); } .blog-article-unified .blog-download-table thead th { background: rgba(255, 255, 255, 0.96); color: #3b3b3b; border-bottom: 1px solid rgba(0, 0, 0, 0.08); } .blog-article-unified .cta-separator { margin-top: 2.4rem; padding-top: 1.8rem; border-top: 1px solid rgba(0, 0, 0, 0.08); } .blog-article-unified .cta-section-minimal { margin-top: 3.5rem; background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.74)); border-color: rgba(0, 0, 0, 0.08); } .section-kicker { display: inline-block; margin-bottom: 0.75rem; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-gold-dark); background: rgba(255, 224, 102, 0.2); padding: 0.34rem 0.9rem; border-radius: 100px; } .section-title { font-size: clamp(1.5rem, 2.3vw, 2rem); font-weight: 740; letter-spacing: -0.02em; line-height: 1.2; margin: 0 0 0.95rem 0; color: #1f1f1f; } .article-content h3 { margin: 1.3rem 0 0.65rem; font-size: 1.2rem; color: #222; } .article-content p { margin: 0 0 1rem 0; } .blog-media { margin: 1.2rem 0; border-radius: 14px; overflow: hidden; border: 1px solid rgba(0, 0, 0, 0.06); box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08); } .blog-media img { display: block; width: 100%; height: auto; } .feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 0.8rem; margin-top: 1.2rem; } .feature-item { background: rgba(255, 255, 255, 0.82); border: 1px solid rgba(0, 0, 0, 0.07); border-radius: 14px; padding: 1rem; } .feature-item-head { display: flex; align-items: center; gap: 0.5rem; font-weight: 700; margin-bottom: 0.5rem; color: #1f1f1f; line-height: 1.35; } .feature-item p { margin: 0; color: #5b5b5b; font-size: 0.94rem; line-height: 1.5; } .feature-icon { display: none; } .feature-icon-image { width: 18px; height: 18px; object-fit: contain; flex-shrink: 0; opacity: 0.95; } .model-showcase { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 0.8rem; margin-top: 1.1rem; } .model-card-enhanced { background: rgba(255, 255, 255, 0.82); border: 1px solid rgba(0, 0, 0, 0.07); border-radius: 16px; padding: 1.15rem; box-shadow: 0 8px 16px rgba(0, 0, 0, 0.04); } .model-title { margin: 0 0 0.7rem; font-size: 1.1rem; font-weight: 700; display: flex; align-items: center; gap: 0.45rem; } .model-badge { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: #675100; background: rgba(255, 224, 102, 0.34); border-radius: 999px; padding: 0.2rem 0.56rem; } .model-stat-grid { margin-top: 0.85rem; border-top: 1px dashed rgba(0, 0, 0, 0.11); padding-top: 0.7rem; display: grid; gap: 0.4rem; } .model-stat-row { display: flex; justify-content: space-between; gap: 0.6rem; font-size: 0.92rem; color: #555; } .blog-callout { margin-top: 1rem; background: linear-gradient(180deg, rgba(255, 224, 102, 0.22), rgba(255, 224, 102, 0.14)); border: 1px solid rgba(255, 224, 102, 0.48); border-radius: 14px; padding: 0.95rem 1rem; } .blog-callout-title { margin: 0 0 0.4rem; font-size: 0.95rem; font-weight: 700; color: #4d3a00; } .blog-callout p { margin: 0; color: #4f4f4f; } .blog-download-table { width: 100%; border-collapse: separate; border-spacing: 0; margin: 0.85rem 0 0.7rem; background: rgba(255, 255, 255, 0.76); border: 1px solid rgba(0, 0, 0, 0.07); border-radius: 14px; overflow: hidden; } .blog-download-table th, .blog-download-table td { text-align: left; vertical-align: top; padding: 0.72rem 0.85rem; font-size: 0.92rem; line-height: 1.5; } .blog-download-table th { background: rgba(255, 224, 102, 0.14); color: #3a3a3a; font-size: 0.8rem; letter-spacing: 0.05em; text-transform: uppercase; } .blog-download-table tbody tr + tr td { border-top: 1px solid rgba(0, 0, 0, 0.07); } .blog-download-table td:first-child { width: 180px; white-space: nowrap; } .blog-download-note { margin: 0.3rem 0 1rem; font-size: 0.9rem; color: #5a5a5a; } .blog-steps-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0.8rem; } .step-card { background: rgba(255, 255, 255, 0.82); border: 1px solid rgba(0, 0, 0, 0.07); border-radius: 14px; padding: 0.95rem; } .step-card h4 { margin: 0 0 0.5rem; font-size: 1rem; color: #3f3200; } .step-card p { margin: 0; color: #5b5b5b; font-size: 0.93rem; } .enhanced-code-block { background: #191a2e; border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 14px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16); padding: 1rem; margin: 0.9rem 0; } .code-header { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.6rem; } .blog-copy-btn { border: 1px solid rgba(255, 255, 255, 0.28); background: rgba(255, 255, 255, 0.08); color: rgba(255, 255, 255, 0.9); border-radius: 9px; padding: 0.3rem 0.62rem; font-size: 0.74rem; font-weight: 650; letter-spacing: 0.02em; cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease; } .blog-copy-btn:hover, .blog-copy-btn:focus { background: rgba(255, 255, 255, 0.14); border-color: rgba(255, 224, 102, 0.45); transform: translateY(-1px); } .blog-copy-btn.is-copied { color: #fff2b3; border-color: rgba(255, 224, 102, 0.55); background: rgba(255, 224, 102, 0.18); } .code-title { color: rgba(255, 255, 255, 0.85); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; } .code-dots { display: flex; gap: 0.32rem; } .code-dot { width: 9px; height: 9px; border-radius: 50%; } .code-dot.red { background: #ff5f56; } .code-dot.yellow { background: #ffbd2e; } .code-dot.green { background: #27ca3f; } .enhanced-code-block pre, .enhanced-code-block code { margin: 0; color: #fff; font-size: 0.9rem; line-height: 1.5; font-family: "Consolas", "Monaco", "Courier New", monospace; background: transparent; padding: 0; } .cta-section { margin-top: 1.2rem; background: linear-gradient(135deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 58%, var(--primary-yellow-darker) 100%); border-radius: 20px; padding: 1.8rem; box-shadow: 0 10px 24px rgba(0, 0, 0, 0.1), 0 2px 0 rgba(255, 255, 255, 0.3) inset; } .cta-content { width: 100%; max-width: 760px; margin: 0 auto; text-align: center; } .cta-title { margin: 0; font-size: 1.75rem; letter-spacing: -0.02em; color: #1f1f1f; } .cta-subtitle { margin: 0.6rem auto 0; max-width: 680px; color: #333; text-align: center; } .cta-links { margin-top: 1.2rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 0.65rem; } .cta-link { display: inline-flex; justify-content: center; align-items: center; gap: 0.38rem; min-height: 54px; border-radius: 12px; border: 1px solid rgba(0, 0, 0, 0.08); background: rgba(255, 255, 255, 0.86); color: #1f1f1f; text-decoration: none; font-weight: 700; transition: transform 0.2s ease, box-shadow 0.2s ease; } .cta-link:first-child { background: #181818; color: #fff; border-color: rgba(255, 255, 255, 0.18); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.22); } .cta-link:hover, .cta-link:focus { transform: translateY(-1px); box-shadow: 0 8px 18px rgba(0, 0, 0, 0.14); color: #1f1f1f; } .cta-link:first-child:hover, .cta-link:first-child:focus { color: #fff; box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3); } .cta-note { margin-top: 1rem; border-radius: 12px; background: rgba(255, 255, 255, 0.78); border: 1px solid rgba(255, 255, 255, 0.5); padding: 0.75rem 0.9rem; color: #2f2f2f; font-weight: 600; } .cta-section-minimal .cta-actions { margin-top: 1rem; display: flex; gap: 0.65rem; justify-content: center; flex-wrap: wrap; } .cta-section-minimal { background: linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.56)); border: 1px solid rgba(0, 0, 0, 0.06); box-shadow: 0 12px 28px rgba(0, 0, 0, 0.05); } .cta-section-minimal .cta-content { display: flex; flex-direction: column; align-items: center; } .cta-section-minimal .cta-subtitle { width: min(680px, 100%); margin: 0.6rem 0 0; text-align: center; } .cta-section-minimal .download-btn { margin: 0; } .cta-section-minimal .cta-action-primary, .cta-section-minimal .cta-action-secondary { min-width: 240px; min-height: 52px; border-radius: 12px; padding: 0.72rem 1.05rem; text-decoration: none; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease; } .cta-section-minimal .cta-action-primary { background: #181818; color: #fff; border: 1px solid rgba(255, 255, 255, 0.16); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.22); } .cta-section-minimal .cta-action-primary:hover, .cta-section-minimal .cta-action-primary:focus { color: #fff; transform: translateY(-1px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.28); } .cta-section-minimal .cta-action-secondary { background: rgba(255, 255, 255, 0.86); color: #1f1f1f; border: 1px solid rgba(0, 0, 0, 0.08); } .cta-section-minimal .cta-action-secondary:hover, .cta-section-minimal .cta-action-secondary:focus { color: #1f1f1f; transform: translateY(-1px); box-shadow: 0 8px 18px rgba(0, 0, 0, 0.14); } .article-navigation { margin-top: 1.1rem; text-align: center; } .back-to-news-btn { min-width: min(250px, 80vw); max-width: min(250px, 80vw); height: 56px; margin: 0 auto; display: inline-flex; align-items: center; justify-content: center; gap: 0.42rem; padding: 0.78rem 1.2rem; border-radius: 14px; font-size: 0.9rem; font-weight: 700; line-height: 1; letter-spacing: 0.01em; color: var(--text-primary); text-decoration: none; background: linear-gradient(135deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 58%, var(--primary-yellow-darker) 100%); border: none; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1), 0 2px 0 rgba(255, 255, 255, 0.3) inset; transition: transform 0.2s ease, box-shadow 0.2s ease; } .back-to-news-btn:hover, .back-to-news-btn:focus { box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15), 0 2px 0 rgba(255, 255, 255, 0.4) inset; transform: translateY(-2px) scale(1.01); color: var(--text-primary); } @media (max-width: 900px) { .blog-article { padding: 1rem; } .blog-article .hero-section { padding: 2rem 1.2rem 1.7rem; } .section-card { padding: 1.2rem; } .blog-article-unified .section-card { padding: 1rem 0; } .blog-article-unified .hero-section { margin-bottom: 1.8rem; } .blog-article-arcade .model-stat-grid { grid-template-columns: 1fr; } .blog-article-arcade .arcade-demo-media { width: 100%; } } @media (max-width: 640px) { .hero-meta { justify-content: flex-start; } .meta-item { width: 100%; text-align: center; } .meta-item.meta-authors { text-align: center; padding-left: 0; padding-right: 0; } } /* ============================================================= HOMEPAGE v6 โ€” tighter layout + stronger section hierarchy ============================================================= */ .hp-main { max-width: none; padding-top: 0; } /* --- Split Hero --- */ .hp-hero { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: 2rem; align-items: center; width: 100%; max-width: 1120px; margin: 0 auto; padding: 7rem 1.5rem 9rem; min-height: auto; position: relative; } .hp-hero-left { display: flex; flex-direction: column; justify-content: center; gap: 1.15rem; max-width: 560px; } .hp-subtitle-group { display: flex; flex-direction: column; gap: 1rem; width: min(460px, 100%); } .hp-headline { font-size: clamp(2rem, 4.1vw, 3.25rem); font-weight: 400; color: #171717; line-height: 1.08; margin: 0; letter-spacing: -0.035em; } .hp-headline-top { display: inline-flex; align-items: baseline; gap: 0.22em; white-space: nowrap; } .hp-carousel-wrap { --hp-carousel-line-height: 1.14; position: relative; display: inline-block; vertical-align: baseline; overflow: hidden; text-align: left; line-height: var(--hp-carousel-line-height); padding-right: 0.06em; padding-bottom: 0.08em; } .hp-carousel-sizer { display: inline-block; visibility: hidden; color: transparent; pointer-events: none; user-select: none; font-weight: 700; line-height: var(--hp-carousel-line-height); white-space: nowrap; } .hp-carousel-track { position: absolute; top: 0; left: 0; right: 0; bottom: 0; } .hp-carousel-slot { position: absolute; top: 0; left: 0; display: block; color: var(--accent-gold-dark); font-weight: 700; white-space: nowrap; text-align: left; line-height: var(--hp-carousel-line-height); transform: translate3d(0, 100%, 0); will-change: transform; } .hp-subtitle { font-size: clamp(0.95rem, 1.8vw, 1.15rem); color: #474747; margin: 0; line-height: 1.5; font-weight: 450; } .hp-cta-row { display: flex; gap: 0.85rem; width: 100%; max-width: 460px; } .hp-cta-row .download-btn, .hp-cta-row .dev-btn { flex: 1 1 0; min-width: 0; margin: 0; padding: 0.8rem 1rem; font-size: 1rem; text-align: center; line-height: 1.1; } .hp-cta-row .download-sub, .hp-cta-row .dev-sub { font-size: 0.75rem; } .hp-hero-right { display: flex; align-items: center; justify-content: center; } .hp-demo-panel { width: 100%; max-width: 440px; background: #1a1b30; border-radius: 18px; padding: 1.4rem; height: 290px; position: relative; overflow: hidden; box-shadow: 0 18px 44px rgba(0, 0, 0, 0.18), 0 4px 10px rgba(0, 0, 0, 0.08); border: 1px solid rgba(255, 255, 255, 0.08); display: flex; flex-direction: column; justify-content: center; } .hp-demo-item { display: flex; flex-direction: column; gap: 0.5rem; height: calc(100% - 2.8rem); opacity: 0; transform: translateY(10px); transition: opacity 0.4s ease, transform 0.4s ease; position: absolute; inset: 1.4rem 1.75rem; pointer-events: none; } .hp-demo-item.active { opacity: 1; transform: translateY(0); pointer-events: auto; } .hp-demo-item.exiting-up { opacity: 0; transform: translateY(-10px); pointer-events: none; } .hp-demo-label { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: rgba(255, 255, 255, 0.55); margin-bottom: 0.2rem; } .hp-demo-chat { display: flex; flex-direction: column; justify-content: space-between; gap: 0.5rem; height: 100%; } .hp-chat-user { background: rgba(255, 224, 102, 0.15); color: #ffe066; font-size: 0.82rem; padding: 0.66rem 0.9rem; border-radius: 15px 15px 15px 5px; line-height: 1.38; align-self: flex-start; max-width: 78%; width: fit-content; } .hp-demo-prompt { max-width: 78%; align-self: flex-start; } .hp-chat-ai { background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.78); font-size: 0.82rem; padding: 0.6rem 0.75rem; border-radius: 10px 10px 3px 10px; line-height: 1.4; align-self: flex-end; max-width: 90%; } .hp-demo-image { display: flex; flex-direction: column; justify-content: space-between; gap: 0.7rem; height: 100%; } .hp-demo-image-placeholder { width: clamp(220px, 68vw, 300px); aspect-ratio: 2 / 1; height: auto; flex-shrink: 0; border-radius: 12px; align-self: flex-end; background: #1f2035 url("https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/generated_image.png") center/contain no-repeat; } @keyframes hp-gradient-shift { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } } .hp-demo-image-prompt { font-size: 0.82rem; color: rgba(255, 255, 255, 0.55); font-style: italic; line-height: 1.4; } .hp-demo-audio { display: flex; flex-direction: column; justify-content: flex-start; gap: 0.7rem; height: 100%; } .hp-waveform { display: flex; align-items: center; justify-content: center; gap: 3px; height: 96px; width: 82%; max-width: 82%; align-self: center; border-radius: 12px; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.08); padding: 0.75rem 1rem; margin-top: 20px; flex-shrink: 0; } .hp-waveform span { display: block; width: 4px; border-radius: 2px; background: var(--primary-yellow); opacity: 0.82; animation: hp-wave 1.2s ease-in-out infinite; } .hp-waveform span:nth-child(1) { height: 30%; animation-delay: 0.00s; } .hp-waveform span:nth-child(2) { height: 55%; animation-delay: 0.05s; } .hp-waveform span:nth-child(3) { height: 80%; animation-delay: 0.10s; } .hp-waveform span:nth-child(4) { height: 60%; animation-delay: 0.15s; } .hp-waveform span:nth-child(5) { height: 95%; animation-delay: 0.20s; } .hp-waveform span:nth-child(6) { height: 70%; animation-delay: 0.25s; } .hp-waveform span:nth-child(7) { height: 45%; animation-delay: 0.30s; } .hp-waveform span:nth-child(8) { height: 85%; animation-delay: 0.35s; } .hp-waveform span:nth-child(9) { height: 50%; animation-delay: 0.40s; } .hp-waveform span:nth-child(10) { height: 75%; animation-delay: 0.45s; } .hp-waveform span:nth-child(11) { height: 40%; animation-delay: 0.50s; } .hp-waveform span:nth-child(12) { height: 90%; animation-delay: 0.55s; } .hp-waveform span:nth-child(13) { height: 55%; animation-delay: 0.60s; } .hp-waveform span:nth-child(14) { height: 70%; animation-delay: 0.65s; } .hp-waveform span:nth-child(15) { height: 35%; animation-delay: 0.70s; } .hp-waveform span:nth-child(16) { height: 80%; animation-delay: 0.75s; } .hp-waveform span:nth-child(17) { height: 50%; animation-delay: 0.80s; } .hp-waveform span:nth-child(18) { height: 65%; animation-delay: 0.85s; } .hp-waveform span:nth-child(19) { height: 40%; animation-delay: 0.90s; } .hp-waveform span:nth-child(20) { height: 55%; animation-delay: 0.95s; } .hp-waveform span:nth-child(21) { height: 75%; animation-delay: 1.00s; } .hp-waveform span:nth-child(22) { height: 45%; animation-delay: 1.05s; } .hp-waveform span:nth-child(23) { height: 85%; animation-delay: 1.10s; } .hp-waveform span:nth-child(24) { height: 60%; animation-delay: 1.15s; } .hp-waveform span:nth-child(25) { height: 50%; animation-delay: 1.20s; } @keyframes hp-wave { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(0.35); } } .hp-audio-text { align-self: flex-end; } /* --- Shared section system --- */ .hp-reveal { opacity: 0; transform: translateY(34px); transition: opacity 0.65s cubic-bezier(0.16, 1, 0.3, 1), transform 0.65s cubic-bezier(0.16, 1, 0.3, 1); } .hp-reveal.hp-visible { opacity: 1; transform: translateY(0); } .hp-section { width: 100%; max-width: 1120px; margin: 0 auto; padding: 3.4rem 1.8rem; text-align: center; position: relative; } .hp-section + .hp-section::before { content: ''; display: block; width: 72px; height: 1px; background: rgba(0, 0, 0, 0.1); margin: 0 auto; position: absolute; top: 0; left: 50%; transform: translateX(-50%); } .hp-section-label { display: inline-block; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-gold-dark); background: rgba(255, 224, 102, 0.2); padding: 0.36rem 0.95rem; border-radius: 100px; margin-bottom: 0.95rem; } .hp-section-heading { font-size: clamp(1.65rem, 3.2vw, 2.5rem); font-weight: 750; color: var(--text-primary); margin: 0 0 0.9rem; letter-spacing: -0.03em; line-height: 1.15; } .hp-section-desc { font-size: clamp(0.96rem, 1.65vw, 1.08rem); color: #505050; margin: 0 auto 2rem; max-width: 760px; line-height: 1.62; } /* --- Open source section --- */ .hp-open-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 1.2rem; align-items: start; text-align: left; } @media (min-width: 1000px) { .hp-open-layout { align-items: stretch; } .hp-open-story, .hp-open-stack { height: 100%; } } .hp-open-story, .hp-open-stack { background: linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.56)); border: 1px solid rgba(0, 0, 0, 0.06); border-radius: 20px; padding: 1.4rem; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.05); } .hp-open-story { display: flex; flex-direction: column; } .hp-open-story h3, .hp-open-stack h3 { margin: 0; font-size: 1.15rem; color: #1e1e1e; letter-spacing: -0.01em; } .hp-community-cta { display: grid; grid-template-columns: 1fr; gap: 0.62rem; margin-top: 1.5rem; margin-left: 10px; justify-items: start; } .hp-community-btn { display: flex; align-items: stretch; text-decoration: none; border-radius: 14px; padding: 0; border: 1px solid transparent; transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; overflow: hidden; min-height: 72px; width: 100%; max-width: 270px; } .hp-community-btn:hover { transform: translateY(-2px); } .hp-community-btn-icon-col { width: 3rem; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; background: rgba(255, 255, 255, 0.06); } .hp-community-btn-text { display: flex; flex-direction: column; justify-content: center; padding: 0.62rem 0.9rem 0.58rem; } .hp-community-btn-icon { display: inline-flex; width: 1.55rem; height: 1.55rem; opacity: 0.95; } .hp-community-btn-icon img, .hp-community-btn-icon svg { width: 100%; height: 100%; display: block; object-fit: contain; fill: currentColor; } .hp-community-btn-label { font-size: 1.08rem; font-weight: 700; line-height: 1.16; } .hp-community-btn-sub { margin-top: 0.2rem; font-size: 0.92rem; line-height: 1.2; font-weight: 600; letter-spacing: 0.01em; } .hp-community-btn-github { background: #181818; color: #fff; border-color: rgba(255, 255, 255, 0.08); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.17); } .hp-community-btn-github .hp-community-btn-sub { color: rgba(255, 255, 255, 0.72); } .hp-community-btn-github .hp-community-btn-icon-col { background: rgba(255, 255, 255, 0.07); } .hp-community-btn-discord { background: linear-gradient(145deg, #6677f9, #5865f2); color: #fff; box-shadow: 0 8px 16px rgba(88, 101, 242, 0.24); } .hp-community-btn-discord .hp-community-btn-sub { color: rgba(255, 255, 255, 0.8); } .hp-community-btn-discord .hp-community-btn-icon-col { background: rgba(255, 255, 255, 0.12); } .hp-community-btn-github:hover { border-color: rgba(255, 224, 102, 0.36); box-shadow: 0 12px 20px rgba(0, 0, 0, 0.2); } .hp-community-btn-discord:hover { box-shadow: 0 12px 22px rgba(88, 101, 242, 0.3); } .hp-logo-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); justify-items: center; align-items: center; gap: 0.45rem 0.72rem; margin-top: 0.95rem; /* Partner logo tuning controls (desktop defaults). */ --hp-logo-lockup-width: 176px; --hp-logo-base-height: 34px; --hp-logo-scale-1: 1.3; /* llama.cpp */ --hp-logo-scale-2: 2; /* ONNX Runtime */ --hp-logo-scale-3: 1.08; /* FastFlowLM */ --hp-logo-scale-4: 1.5; /* Ryzen AI */ --hp-logo-scale-5: 1.3; /* ROCm */ --hp-logo-scale-6: 2; /* Hugging Face */ --hp-logo-scale-7: 1.1; /* Vulkan */ --hp-logo-scale-8: 1.25; /* whisper.cpp */ --hp-logo-scale-9: 2.5; /* stable-diffusion.cpp */ --hp-logo-scale-10: 2.3; /* Kokoros */ } .hp-logo-chip { min-height: 70px; display: flex; justify-content: center; align-items: center; padding: 0.26rem 0.32rem; text-decoration: none; filter: grayscale(0.04) saturate(0.97); transition: transform 0.2s ease, filter 0.2s ease, opacity 0.2s ease, background-color 0.2s ease; opacity: 0.94; position: relative; border-radius: 10px; --hp-scale: 1; width: 100%; } .hp-logo-chip::before { content: ""; position: absolute; inset: 6px; border-radius: 10px; background: linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.06)); opacity: 0; transition: opacity 0.2s ease; } .hp-logo-chip:hover { transform: translateY(-1px); filter: grayscale(0) saturate(1); opacity: 1; } .hp-logo-chip:hover::before { opacity: 1; } .hp-logo-lockup { width: min(var(--hp-logo-lockup-width), 100%); height: auto; display: inline-flex; justify-content: center; align-items: center; overflow: visible; position: relative; z-index: 1; } .hp-logo-chip img { width: auto; max-width: min(182px, 100%); height: max(28px, calc(var(--hp-logo-base-height) * var(--hp-scale))); object-fit: contain; mix-blend-mode: multiply; } .hp-logo-chip:nth-child(1) { --hp-scale: var(--hp-logo-scale-1); } .hp-logo-chip:nth-child(2) { --hp-scale: var(--hp-logo-scale-2); } .hp-logo-chip:nth-child(3) { --hp-scale: var(--hp-logo-scale-3); } .hp-logo-chip:nth-child(4) { --hp-scale: var(--hp-logo-scale-4); } .hp-logo-chip:nth-child(5) { --hp-scale: var(--hp-logo-scale-5); } .hp-logo-chip:nth-child(6) { --hp-scale: var(--hp-logo-scale-6); } .hp-logo-chip:nth-child(7) { --hp-scale: var(--hp-logo-scale-7); } .hp-logo-chip:nth-child(8) { --hp-scale: var(--hp-logo-scale-8); } .hp-logo-chip:nth-child(9) { --hp-scale: var(--hp-logo-scale-9); } .hp-logo-chip:nth-child(10) { --hp-scale: var(--hp-logo-scale-10); } .hp-section-release { margin-top: 1.3rem; } .hp-release-carousel { display: grid; grid-template-columns: auto 700px auto; align-items: center; justify-content: center; gap: 0.25rem; } .hp-release-nav { width: auto; height: auto; border-radius: 0; border: none; background: transparent; color: rgba(33, 33, 33, 0.52); box-shadow: none; display: inline-flex; align-items: center; justify-content: center; padding: 0 0.28rem; cursor: pointer; transition: color 0.2s ease, opacity 0.2s ease; } .hp-release-nav span { font-size: 3.15rem; line-height: 0.95; } .hp-release-nav:hover, .hp-release-nav:focus { color: rgba(33, 33, 33, 0.82); } .hp-release-nav:disabled { opacity: 0.24; cursor: not-allowed; } .hp-release-section .release-announcement { margin: 0 auto; width: 700px; min-width: 700px; max-width: 700px; min-height: 300px; animation: none; background: linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.56)); border: 1px solid rgba(0, 0, 0, 0.06); border-radius: 20px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.05); backdrop-filter: none; padding: 0; } .hp-release-section .release-announcement::before { display: none; } .hp-release-section .release-content { margin-bottom: 0; padding: 1.2rem 1.2rem 0.72rem; } .hp-release-section .release-info { text-align: left; } .hp-release-section .release-name { margin: 0 0 0.3rem 0; font-size: 1.28rem; color: #2a2a2a; } .hp-release-section .release-date { margin: 0 0 0.86rem; } .hp-release-section .release-link { margin: 0 auto 1.05rem; min-width: min(250px, 80vw); max-width: min(250px, 80vw); height: 70px; display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem; padding: 0.78rem 1.2rem; border-radius: 14px; font-size: 0.94rem; font-weight: 700; line-height: 1; letter-spacing: 0.01em; color: var(--text-primary); text-decoration: none; background: linear-gradient(135deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 58%, var(--primary-yellow-darker) 100%); border: none; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1), 0 2px 0 rgba(255, 255, 255, 0.3) inset; transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease; } .hp-release-section .release-link:hover, .hp-release-section .release-link:focus { box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15), 0 2px 0 rgba(255, 255, 255, 0.4) inset; transform: translateY(-2px) scale(1.02); } .hp-release-section .release-link:active { background: var(--primary-yellow); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); transform: scale(0.98); } /* --- Technical details section --- */ .hp-tech-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.9rem; text-align: left; } .hp-tech-card { background: rgba(255, 255, 255, 0.64); border: 1px solid rgba(0, 0, 0, 0.07); border-radius: 15px; padding: 1rem; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.04); } .hp-tech-icon { width: 42px; height: 42px; object-fit: contain; margin-bottom: 0.62rem; } .hp-tech-card h3 { margin: 0 0 0.4rem; font-size: 0.95rem; color: #242424; line-height: 1.3; } .hp-tech-card p { margin: 0; font-size: 0.83rem; line-height: 1.45; color: #535353; } /* --- Unified API section --- */ .hp-api-layout { display: flex; flex-direction: column; align-items: center; gap: 1.1rem; text-align: left; margin-top: 1.25rem; --hp-api-block-width: 700px; } .hp-api-modalities { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 0.7rem; width: min(var(--hp-api-block-width), 100%); } .hp-api-modality { appearance: none; -webkit-appearance: none; background: linear-gradient(145deg, #191a2e, #1f2036); color: rgba(255, 255, 255, 0.95); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 14px; padding: 0.62rem 0.72rem; text-align: left; cursor: pointer; font: inherit; -webkit-tap-highlight-color: transparent; outline: none; position: relative; overflow: hidden; transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; min-height: 52px; } .hp-api-modality:hover { transform: translateY(-1px); border-color: rgba(255, 224, 102, 0.32); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.16); } .hp-api-modality.is-active { border-color: rgba(255, 224, 102, 0.55); background: linear-gradient(145deg, #21233f, #2a2d50); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(255, 224, 102, 0.24); } .hp-api-modality:focus-visible { border-color: rgba(255, 224, 102, 0.42); } .hp-api-modality:active { background: linear-gradient(145deg, #191a2e, #1f2036); transform: translateY(0); } .hp-api-modality.is-active:active { background: linear-gradient(145deg, #1b1d33, #242642); } .hp-api-modality h3 { margin: 0; font-size: 0.92rem; font-weight: 620; color: rgba(255, 255, 255, 0.96); } .hp-api-modality.is-active h3 { color: #fff2b3; } .hp-api-sample-panel { background: #191a2e; color: rgba(255, 255, 255, 0.9); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 16px; padding: 1.1rem 1.15rem; height: auto; width: min(var(--hp-api-block-width), 100%); box-sizing: border-box; display: flex; flex-direction: column; align-self: center; } .hp-api-sample-head { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; } .hp-api-copy-btn { border: 1px solid rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.9); border-radius: 9px; line-height: 0; padding: 0.46rem; cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease; } .hp-api-copy-btn svg { width: 0.95rem; height: 0.95rem; display: block; fill: currentColor; } .hp-api-copy-btn:hover, .hp-api-copy-btn:focus { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 224, 102, 0.42); transform: translateY(-1px); } .hp-api-copy-btn.is-copied { color: #ffe680; border-color: rgba(255, 224, 102, 0.55); background: rgba(255, 224, 102, 0.16); } .hp-api-sample-method { margin: 0; color: rgba(255, 255, 255, 0.72); font-size: 0.92rem; font-weight: 600; gap: 0.48rem; } .hp-api-cta { margin-top: 1.6rem; display: flex; justify-content: center; } .hp-api-cta .marketplace-cta-btn { margin: 0; min-width: min(250px, 80vw); max-width: min(250px, 80vw); height: 70px; display: inline-flex; align-items: center; justify-content: center; line-height: 1.2; } .hp-api-sample-panel pre { margin: 0.68rem 0 0; border-radius: 10px; background: rgba(255, 255, 255, 0.08); padding: 0.86rem 0.95rem; overflow: visible; max-height: none; white-space: pre-wrap !important; overflow-wrap: normal !important; word-break: normal !important; word-wrap: normal !important; } .hp-api-sample-panel code { color: #ffffff; font-size: 0.9rem; line-height: 1.5; font-family: "Consolas", "Monaco", "Courier New", monospace; display: block; min-width: 0; white-space: pre-wrap !important; overflow-wrap: normal !important; word-break: normal !important; word-wrap: normal !important; } .hp-api-method { font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 0.72rem; color: rgba(255, 255, 255, 0.52); margin-bottom: 0.6rem; display: flex; align-items: center; gap: 0.4rem; } .hp-method-badge { background: rgba(255, 224, 102, 0.14); color: #ffe066; font-size: 0.62rem; font-weight: 700; padding: 0.15rem 0.38rem; border-radius: 4px; letter-spacing: 0.04em; } /* --- Ecosystem section --- */ .hp-eco-apps { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 1.2rem 0.85rem; text-align: center; margin-top: 1.35rem; width: min(860px, 100%); margin-left: auto; margin-right: auto; } .hp-eco-app { display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 0.45rem; padding: 0.45rem 0.35rem; border-radius: 12px; text-decoration: none; background: transparent; border: 1px solid transparent; transition: all 0.22s ease; } .hp-eco-app:hover { background: rgba(255, 255, 255, 0.58); transform: translateY(-2px); box-shadow: 0 8px 14px rgba(0, 0, 0, 0.05); border-color: rgba(0, 0, 0, 0.06); } .hp-eco-app img { width: 70px; height: 70px; object-fit: contain; border-radius: 14px; flex-shrink: 0; } .hp-eco-app-name { font-size: 0.82rem; font-weight: 620; color: #1f1f1f; line-height: 1.25; text-align: center; max-width: 100%; } .hp-eco-cta { margin-top: 2.5rem; display: flex; justify-content: center; } .hp-eco-cta .marketplace-cta-btn { margin: 0; min-width: min(250px, 80vw); max-width: min(250px, 80vw); height: 70px; display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem; padding: 0.78rem 1.2rem; border-radius: 14px; font-size: 0.94rem; font-weight: 700; line-height: 1; letter-spacing: 0.01em; color: var(--text-primary); text-decoration: none; background: linear-gradient(135deg, var(--primary-yellow) 0%, var(--primary-yellow-dark) 58%, var(--primary-yellow-darker) 100%); border: none; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1), 0 2px 0 rgba(255, 255, 255, 0.3) inset; transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease; } .hp-eco-cta .marketplace-cta-btn:hover, .hp-eco-cta .marketplace-cta-btn:focus { box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15), 0 2px 0 rgba(255, 255, 255, 0.4) inset; transform: translateY(-2px) scale(1.02); } .hp-eco-cta .marketplace-cta-btn:active { background: var(--primary-yellow); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); transform: scale(0.98); } /* --- Homepage responsive --- */ @media (max-width: 1024px) { .hp-main { margin-top: 0 !important; } } @media (max-width: 980px) { .hp-hero { grid-template-columns: 1fr; justify-items: center; gap: 1.75rem; padding: 2.8rem 1rem 4rem; text-align: center; } .hp-hero-left { text-align: center; align-items: center; width: 100%; max-width: 560px; margin: 0 auto; } .hp-hero-right { width: 100%; margin: 0 auto; } .hp-subtitle-group { width: 100%; max-width: 440px; } .hp-demo-panel { max-width: 440px; height: 280px; margin: 0 auto; } .hp-open-layout { grid-template-columns: 1fr; } .hp-open-story { width: 100%; max-width: 440px; justify-self: center; } .hp-open-story h3 { text-align: center; } .hp-community-cta { margin-left: 0; justify-items: center; } .hp-tech-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .hp-api-modalities { grid-template-columns: repeat(3, minmax(0, 1fr)); } .hp-api-sample-panel { height: auto; } .hp-release-carousel { grid-template-columns: minmax(0, 1fr); justify-items: stretch; gap: 0; position: relative; padding: 0 2.1rem; } .hp-release-nav { position: absolute; top: 50%; transform: translateY(-50%); z-index: 3; padding: 0 0.14rem; } .hp-release-nav span { font-size: 2.1rem; line-height: 1; } .hp-release-nav-prev { left: 0.1rem; } .hp-release-nav-next { right: 0.1rem; } .hp-release-section .release-announcement { width: min(700px, 100%); min-width: 0; max-width: min(700px, 100%); min-height: 300px; } .hp-logo-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.34rem 0.58rem; --hp-logo-base-height: 32px; --hp-logo-lockup-width: 164px; } .hp-logo-chip { min-height: 64px; } .hp-eco-apps { grid-template-columns: repeat(4, minmax(0, 1fr)); } } @media (max-width: 720px) { .hp-section { padding: 2.8rem 1rem; } .hp-headline { font-size: 1.9rem; } .hp-cta-row { flex-direction: column; width: auto; max-width: 460px; align-items: center; } .hp-cta-row .download-btn, .hp-cta-row .dev-btn { width: min(250px, 80vw); min-width: min(250px, 80vw); max-width: min(250px, 80vw); height: 70px; min-height: 70px; padding: 0.8rem 1rem; justify-content: center; } .hp-demo-panel { height: 265px; } .hp-demo-image-placeholder { height: 128px; } .hp-api-modalities { grid-template-columns: repeat(2, minmax(0, 1fr)); } .hp-api-sample-panel code { font-size: 0.84rem; } .hp-logo-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .hp-eco-apps { grid-template-columns: repeat(3, minmax(0, 1fr)); } } @media (max-width: 480px) { .hp-tech-grid { grid-template-columns: 1fr; } .hp-api-modalities { grid-template-columns: 1fr; } .hp-api-sample-panel { height: auto; padding: 0.95rem; } .hp-demo-panel, .hp-open-story, .hp-open-stack, .hp-tech-card, .hp-api-modality, .hp-api-sample-panel, .hp-eco-app { border-radius: 12px; } .hp-logo-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.24rem 0.4rem; --hp-logo-base-height: 30px; --hp-logo-lockup-width: 144px; } .hp-logo-chip { min-height: 58px; } .hp-eco-apps { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.62rem 0.5rem; } } lemonade-sdk-lemonade-dbde812/docs/code.md000066400000000000000000000021161516551144000205200ustar00rootroot00000000000000# Lemonade Code Structure The Lemonade source code has a few major top-level directories: - `.github`: defines CI workflows for GitHub Actions. - `docs`: documentation for the entire project. - `examples`: example scripts and demos. - `src`: source code. - `/app`: Electron desktop application (Model Manager, Chat UI). - `/cpp`: C++ implementation of the Lemonade Server. - `/lemonade`: Python package for the `lemonade-eval` CLI. - `/tools`: implements `Tool` and defines the tools built in to the `lemonade-eval` CLI (e.g., `load`, `bench`, `oga-load`, `lm-eval-harness`, etc.). - `/sequence.py`: implements `Sequence` and defines the plugin API for `Tool`s. - `/cli.py`: implements the `lemonade-eval` CLI entry point. - `/common`: functions common to the other modules. - `/version.py`: defines the package version number. - `/state.py`: implements the `State` class. - `setup.py`: defines the `lemonade-sdk` wheel. - `test`: tests for Lemonade. lemonade-sdk-lemonade-dbde812/docs/contribute.md000066400000000000000000000114151516551144000217660ustar00rootroot00000000000000# Lemonade SDK Contribution Guide The Lemonade SDK project welcomes contributions from everyone! See [code organization](https://github.com/lemonade-sdk/lemonade/blob/main/docs/code.md) for an overview of the repository. ## Collaborate with Your App Lemonade Server integrates quickly with most OpenAI-compatible LLM apps. You can: - Share an example of your app using Lemonade via [Discord](https://discord.gg/5xXzkMu8Zk), [GitHub Issue](https://github.com/lemonade-sdk/lemonade/issues), or [email](mailto:lemonade@amd.com). - Contribute a guide by adding a `.md` file to the [server apps folder](https://github.com/lemonade-sdk/lemonade/tree/main/docs/server/apps). Follow the style of the [Open WebUI guide](./server/apps/open-webui.md). Guides should: - Work in under 10 minutes. - Require no code changes to the app. - Use OpenAI API-compatible apps with configurable base URLs. ## Backend Contributions To contribute code or examples, first open an [Issue](https://github.com/lemonade-sdk/lemonade/issues) with: - A descriptive title. - Relevant labels (`enhancement`, `good first issue`, etc.). - A proposal explaining what you're contributing. - The use case it supports. One of the maintainers will get back to you ASAP with guidance. ## UI/Frontend Contributions **Current UI Development Approach:** For now, UI and frontend development is being handled exclusively by core maintainers. Here's why: AI-assisted coding has made building UIs incredibly fast, but it's also made reviewing UI PRs quite challenging. UI changes often involve complex state management, visual consistency, accessibility considerations, and cross-platform considerations that require deep context about the entire application architecture. **How You Can Still Influence the UI:** We want your creativity and insights! Share UI/UX ideas, report bugs, or request features via [Issue](https://github.com/lemonade-sdk/lemonade/issues) or [Discord](https://discord.gg/5xXzkMu8Zk). Include mockups, screenshots, and reproduction steps where relevant. **UI Scope: Management, Not Competition:** Our UI exists to facilitate Lemonade management - not to compete with the apps built on top of Lemonade. While it's tempting to add agentic workflows, advanced chat features, or other sophisticated capabilities, that's not our goal. We focus on making model management, configuration, and monitoring delightful and effortless. Defining this line isn't always easy, but use this principle as your guide when considering new UI features. ## Issues Use [Issues](https://github.com/lemonade-sdk/lemonade/issues) to report bugs or suggest features. A maintainer will apply one of these labels to indicate the status: - `on roadmap`: planned for development. - `good first issue`: open for contributors. - `needs detail`: needs clarification before proceeding. - `wontfix`: out of scope or unmaintainable. ## Pull Requests Submit a PR to contribute code. Maintainers: - @danielholanda - @jeremyfowers - @ramkrishna2910 - @vgodsoe Discuss major changes via an Issue first. ## Code Formatting We require that all Python files in this repo adhere to black formatting. This is enforced with a black check in CI workflows. ### Running Black formatting The easiest way to ensure proper formatting is to enable the black formatter in VSCode with format-on-save: 1. **Install the Python extension**: Install the Python extension for VSCode if you haven't already. 2. **Set black as the default formatter**: - Open VSCode settings (Ctrl/Cmd + ,) - Search for "Formatter" - Set the Python default formatter to "black" 3. **Enable format-on-save**: - In VSCode settings, search for "format on save" - Check the "Format On Save" option This will automatically format your code according to black standards whenever you save a file. #### Alternative Setup You can also install black directly and run it manually: ```bash # Install black (if not already installed) pip install black # Run black formatter on your file black your_file.py ``` ### Linting We use linting tools to maintain code quality and catch potential issues. The project uses standard Python linting tools that are automatically run in CI. #### Running Linters Locally To run linting checks locally before submitting a PR: ```bash # Install linting dependencies (if not already installed) pip install pylint # Run pylint from the root of the repo pylint src/lemonade --rcfile .pylintrc --disable E0401 ``` This will show linting warnings and errors in your terminal. ## Testing Tests are run automatically on each PR. These include: - Linting - Code formatting (`black`) - Unit tests - End-to-end tests To run tests locally, use the commands in `.github/workflows/`. ## Versioning We follow [Semantic Versioning](https://github.com/lemonade-sdk/lemonade/blob/main/docs/versioning.md). lemonade-sdk-lemonade-dbde812/docs/dev-getting-started.md000066400000000000000000001047151516551144000234770ustar00rootroot00000000000000# Lemonade Development This guide covers everything you need to build, test, and contribute to Lemonade from source. Whether you're fixing a bug, adding a feature, or just exploring the codebase, this document will help you get started. ## Table of Contents - [Components](#components) - [Building from Source](#building-from-source) - [Prerequisites](#prerequisites) - [Build Steps](#build-steps) - [Build Outputs](#build-outputs) - [Building the Electron Desktop App (Optional)](#building-the-electron-desktop-app-optional) - [Platform-Specific Notes](#platform-specific-notes) - [Building Installers](#building-installers) - [Windows Installer (WiX/MSI)](#windows-installer-wixmsi) - [Linux .deb Package (Debian/Ubuntu)](#linux-deb-package-debianubuntu) - [Linux .rpm Package (Fedora, RHEL etc)](#linux-rpm-package-fedora-rhel-etc) - [Developer IDE & IDE Build Steps](#developer-ide--ide-build-steps) - [Code Structure](#code-structure) - [Architecture Overview](#architecture-overview) - [Overview](#overview) - [Client-Server Communication](#client-server-communication) - [Internal Endpoints](#internal-endpoints) - [Dependencies](#dependencies) - [Usage](#usage) - [lemond (Server Only)](#lemond-server-only) - [lemonade-server.exe (Console CLI Client)](#lemonade-serverexe-console-cli-client) - [lemonade-tray (GUI Tray Application)](#lemonade-tray-gui-tray-application---windows-and-linux) - [Logging and Console Output](#logging-and-console-output) - [Testing](#testing) - [Basic Functionality Tests](#basic-functionality-tests) - [Integration Tests](#integration-tests) - [Development](#development) - [Code Style](#code-style) - [Key Resources](#key-resources) - [License](#license) ## Components Lemonade consists of these main executables: - **lemond** - Core HTTP server that handles requests and LLM backend orchestration - **lemonade** - CLI client for terminal users (list, pull, delete, run, status, logs, launch, backends, scan) - **LemonadeServer.exe** (Windows only) - SUBSYSTEM:WINDOWS GUI app that embeds the server and shows a system tray icon - **lemonade-tray** (macOS/Linux) - Lightweight tray client that connects to a running `lemond` - **lemonade-server** - Deprecated backwards-compatibility shim (delegates to `lemond` or `lemonade`) ## Building from Source ### Prerequisites **All Platforms:** - CMake 3.28 or higher - C++17 compatible compiler - Git (for fetching dependencies) - Internet connection (first build downloads dependencies) **Windows:** - Visual Studio 2022 or later (2022 and 2026 are supported via CMake presets) - WiX 5.x (only required for building the installer) **Linux:** - Ninja build system (optional, recommended) ### Build Steps A helper script is available that will set up the build environment on popular Linux distributions and macOS. This will prompt to install dependencies via native package managers and create the build directory. **Linux / macOS** ```bash ./setup.sh ``` **Windows** ```shell ./setup.ps1 ``` Build by running: **Linux / macOS** ```bash cmake --build --preset default ``` **Windows (Visual Studio 2022)** ```powershell cmake --build --preset windows ``` **Windows (Visual Studio 2026)** ```powershell cmake --build --preset vs18 ``` ### Build Outputs - **Windows:** - `build/Release/lemond.exe` - HTTP server - `build/Release/LemonadeServer.exe` - GUI app (embedded server + system tray) - `build/Release/lemonade.exe` - CLI client - `build/Release/lemonade-server.exe` - Legacy shim (deprecated) - **Linux/macOS:** - `build/lemond` - HTTP server - `build/lemonade` - CLI client - `build/lemonade-tray` - System tray client (macOS always; Linux when AppIndicator3 found) - `build/lemonade-server` - Legacy shim (deprecated) - **Resources:** Automatically copied to `build/Release/resources/` on Windows, `build/resources/` on Linux/macOS (web UI files, model registry, backend version configuration) ### Building the Electron Desktop App (Optional) The tray menu's "Open app" option and the `lemonade run` command can launch the Electron desktop app. To include it in your build: Build the Electron app using CMake (requires Node.js 20+): **Linux** ```bash cmake --build --preset default --target electron-app ``` **Windows (Visual Studio 2022)** ```powershell cmake --build --preset windows --target electron-app ``` **Windows (Visual Studio 2026)** ```powershell cmake --build --preset vs18 --target electron-app ``` This will: 1. Copy src/app to build/app-src (keeps source tree clean) 2. Run npm install in build/app-src 3. Build to build/app/linux-unpacked/ (Linux) or build/app/win-unpacked/ (Windows) The tray app searches for the Electron app in these locations: - **Windows installed**: `../app/lemonade-app.exe` (relative to bin/ directory) - **Windows development**: `../app/win-unpacked/lemonade-app.exe` (from build/Release/) - **Linux installed**: `/opt/share/lemonade-server/app/lemonade-app` - **Linux development**: `../app/linux-unpacked/lemonade-app` (from build/) If not found, the "Open app" menu option is hidden but everything else works. ### Building an AppImage (Linux Only) AppImage builds are opt-in. Enable the option at configure time and then build: ```bash cmake --preset default -DBUILD_APPIMAGE=ON cmake --build --preset default --target appimage ``` Alternatively, if you've already configured without the flag, you can still trigger a one-off build using the manual target: ```bash cmake --build --preset default --target appimage ``` This will: 1. Copy the Electron app source to a separate build directory 2. Set the package.json version to match the CMake project version 3. Install npm dependencies 4. Build the renderer with production optimizations 5. Package the application as an AppImage using electron-builder The generated AppImage will be located in: - `build/app-appimage/lemonade-app--.AppImage` The AppImage is a self-contained executable that includes all dependencies and can be run on any Linux distribution without installation. Simply make it executable and run it: ```bash chmod +x build/app-appimage/lemonade-app-*.AppImage ./build/app-appimage/lemonade-app-*.AppImage ``` ### Platform-Specific Notes **Windows:** - The build uses static linking to minimize DLL dependencies - All dependencies are built from source (no external DLL requirements) - Security features enabled: Control Flow Guard, ASLR, DEP **Linux:** - `lemond` is always headless on Linux (GTK-free, daemon-friendly); use `lemond` to start the server directly - `lemonade-tray` is a separate binary for the system tray, auto-detected at build time: built if AppIndicator3 libraries are found (GTK3 only needed for non-glib variants) - To require tray support (fail if deps missing): `-DREQUIRE_LINUX_TRAY=ON` - Optional tray dependencies: one of `ayatana-appindicator-glib-devel` (preferred, no GTK3 needed), `ayatana-appindicator3-devel`, or `libappindicator-gtk3-devel` (the latter two also require `gtk3-devel`) - Fully functional for server operations and model management - Uses permissively licensed dependencies only (MIT, Apache 2.0, BSD, curl license) - Clean .deb package with only runtime files (no development headers) - Proper graceful shutdown - all child processes cleaned up correctly - File locations: - Installed binaries: `/opt/bin` - Downloaded backends (llama-server, ryzenai-server): `~/.cache/lemonade/bin/` - Model downloads: `~/.cache/huggingface/` (follows HF conventions) - Runtime files (lock, log): `$XDG_RUNTIME_DIR/lemonade/` when set and writable, otherwise `/tmp/` **macOS (beta):** - Uses native system frameworks (Cocoa, Foundation) - ARM Macs use Metal backend by default for llama.cpp - macOS support is currently in beta; a signed and notarized `.pkg` installer is available from the [releases page](https://github.com/lemonade-sdk/lemonade/releases/latest) ## Building Installers ### Windows Installer (WiX/MSI) **Prerequisites:** - WiX Toolset 5.0.2 installed from [wix-cli-x64.msi](https://github.com/wixtoolset/wix/releases/download/v5.0.2/wix-cli-x64.msi) - Completed C++ build (see above) **Building:** ```powershell cmake --build build --config Release --target wix_installers ``` **Installer Output:** Creates `lemonade-server-minimal.msi` which: - MSI-based installer (Windows Installer technology) - **Per-user install (default):** Installs to `%LOCALAPPDATA%\lemonade_server\`, adds to user PATH, no UAC required - **All-users install (CLI only):** Installs to `%PROGRAMFILES%\Lemonade Server\`, adds to system PATH, requires elevation - Creates Start Menu shortcuts (launches `lemonade-tray.exe`) - Optionally creates desktop shortcut and startup entry - Uses Windows Installer Restart Manager to gracefully close running processes - Includes all core executables (router, server, tray, CLI, and optional desktop app) - Proper upgrade handling between versions - Includes uninstaller **Available Installers:** - `lemonade-server-minimal.msi` - Server only (~3 MB) - `lemonade.msi` - Full installer with Electron desktop app (~105 MB) **Installation:** For detailed installation instructions including silent install, custom directories, and all-users installation, see the [Server Integration Guide](../../docs/server/server_integration.md#windows-installation). ### Linux .deb Package (Debian/Ubuntu) **Prerequisites:** - Completed C++ build (see above) **Building:** ```bash cd build cpack ``` **Package Output:** Creates `lemonade-server__amd64.deb` (e.g., `lemonade-server_9.0.3_amd64.deb`) which: - Installs to `/opt/bin/` (executables) - Installs resources to `/opt/share/lemonade-server/` - Creates desktop entry in `/opt/share/applications/` - Declares dependencies: `libcurl4`, `libssl3`, `libz1`, `unzip`, `fonts-katex` - Recommends: `ffmpeg` for whisper.cpp audio resampling and/or transcoding, plus a Chromium-compatible browser for `lemonade-web-app` - Package size: ~2.2 MB (clean, runtime-only package) - Includes postinst script that creates writable `/opt/share/lemonade-server/llama/` directory **Installation:** ```bash # Replace with the actual version (e.g., 9.0.0) sudo apt install ./lemonade-server__amd64.deb ``` **Uninstallation:** ```bash sudo dpkg -r lemonade-server ``` **Post-Installation:** The executables will be available in PATH: ```bash lemonade --help lemond --help ``` ### Linux .rpm Package (Fedora, RHEL etc) Very similar to the Debian instructions above with minor changes **Building:** ```bash cd build cpack -G RPM ``` **Package Output:** Creates `lemonade-server-.x86_64.rpm` (e.g., `lemonade-server-9.1.2.x86_64.rpm`) and resources are installed as per DEB version above **Installation:** ```bash # Replace with the actual version (e.g., 9.0.0) sudo dnf install ./lemonade-server-.x86_64.rpm ``` **Uninstallation:** ```bash sudo dnf remove lemonade-server ``` **Post-Installation:** Same as .deb above **macOS:** ### Building from Source on MacOS for M-Series / arm64 Family #### Macos Notary Tool Command For access with P ``` xcrun notarytool store-credentials AC_PASSWORD --apple-id "your-apple-id@example.com" --team-id "your-team-id" --private-key "/path/to/AuthKey_XXXXXX.p8" ``` or For access with API password ``` xcrun notarytool store-credentials AC_PASSWORD --apple-id "your-apple-id@example.com" --team-id "your-team-id" --password "" ``` Get your team id at: https://developer.apple.com/account #### Cmake build instructions ```bash # Install Xcode command line tools xcode-select --install # Navigate to the C++ source directory cd src/cpp # Create and enter build directory mkdir build cd build # Configure with CMake cmake .. # Build with all cores cmake --build . --config Release -j ``` ### CMake Targets The build system provides several CMake targets for different build configurations: - **`lemond`**: The main HTTP server executable that handles LLM inference requests - **`package-macos`**: Creates a signed macOS installer package (.pkg) using productbuild - **`notarize_package`**: Builds and submits the package to Apple for notarization and staples the ticket - **`electron-app`**: Builds the Electron-based GUI application - **`prepare_electron_app`**: Prepares the Electron app for inclusion in the installer ### Building and Notarizing for Distribution To build a notarized macOS installer for distribution: 1. **Prerequisites**: - Apple Developer Program membership - Valid Developer ID Application and Installer certificates - App-specific password for notarization - Xcode command line tools 2. **Set Environment Variables**: ```bash export DEVELOPER_ID_APPLICATION_IDENTITY="Developer ID Application: Your Name (TEAMID)" export DEVELOPER_ID_INSTALLER_IDENTITY="Developer ID Installer: Your Name (TEAMID)" export AC_PASSWORD="your-app-specific-password" ``` 3. **Configure Notarization Keychain Profile**: ```bash xcrun notarytool store-credentials "AC_PASSWORD" \ --apple-id "your-apple-id@example.com" \ --team-id "YOURTEAMID" \ --password "your-app-specific-password" ``` 4. **Build and Notarize**: ```bash cd src/cpp/build cmake --build . --config Release --target package-macos cmake --build . --config Release --target notarize_package ``` The notarization process will: - Submit the package to Apple's notarization service - Wait for approval - Staple the notarization ticket to the package **Note**: The package is signed with hardened runtime entitlements during the build process for security. ### Developer IDE & IDE Build Steps #### Visual Studio Code Setup Guide 1. Clone the repository into a blank folder locally on your computer. 2. Open the folder in visual studio code. 3. Install Dev Containers extension in Visual Studio Code by using control + p to open the command bar at the top of the IDE or if on mac with Cmd + p. 4. Type "> Extensions: Install Extensions" which will open the Extensions side panel. 5. in the extensions search type ```Dev Containers``` and install it. 6. Once completed with the prior steps you may run command ```>Dev Containers: Open Workspace in Container``` or ```>Dev Containers: Open Folder in Container``` which you can do in the command bar in the IDE and it should reopen the visual studio code project. 7. It will launch a docker and start building a new docker and then the project will open in visual studio code. #### Build & Compile Options 1. Assuming your VSCode IDE is open and the dev container is working. 2. Go to the CMake plugin you may select the "Folder" that is where you currently want to build. 3. Once done with that you may select which building toolkit you are using under Configure and then begin configure. 4. Under Build, Test, Debug and/or Launch you may select whatever configuration you want to build, test, debug and/or launch. #### Debug / Runtime / Console arguments 1. You may find arguments which are passed through to the application you are debugging in .vscode/settings.json which will look like the following: ``` "cmake.debugConfig": { "args": [ "--llamacpp", "cpu" ] } ``` 2. If you want to debug lemond you may pass --llamacpp cpu for cpu based tests. 3. For `lemonade` you may pass a subcommand (e.g., `run MODEL`) as arguments. ##### The hard way - commands only. 1. Now if you want to do it the hard way below are the commands in which you can run in the command dropdown in which you can see if you use the following keyboard shortcuts. cmd + p / control + p ``` > Cmake: Select a Kit # Select a kit or Scan for kit. (Two options should be available gcc or clang) > Cmake: Configure # Optional commands are: > Cmake: Build Target # use this to select a cmake target to build > Cmake: Set Launch/Debug target # use this to select/set your cmake target you want to build/debug # This next command lets you debug > Cmake: Debug # This command lets you delete the cmake cache and reconfigure which is rarely needed. > Cmake: Delete Cache and Reconfigure ``` 2. Custom configurations for cmake are in the root directory under ```.vscode/settings.json``` in which you may set custom args for launching the debug in the json key ```cmake.debugConfig``` > **Note** > > For running Lemonade as a containerized application (as an alternative to the MSI-based distribution), see `DOCKER_GUIDE.md`. ## Code Structure ``` src/cpp/ โ”œโ”€โ”€ CopyElectronApp.cmake # CMake module to copy Electron app to build output โ”œโ”€โ”€ CPackRPM.cmake # RPM packaging configuration โ”œโ”€โ”€ DOCKER_GUIDE.md # Docker containerization guide โ”œโ”€โ”€ Extra-Models-Dir-Spec.md # Extra models directory specification โ”œโ”€โ”€ Multi-Model-Spec.md # Multi-model loading specification โ”œโ”€โ”€ postinst # Debian package post-install script โ”œโ”€โ”€ postinst-full # Debian package post-install script (full version) โ”œโ”€โ”€ resources/ # Configuration and data files (self-contained) โ”‚ โ”œโ”€โ”€ backend_versions.json # llama.cpp/whisper version configuration โ”‚ โ”œโ”€โ”€ server_models.json # Model registry (available models) โ”‚ โ””โ”€โ”€ static/ # Web UI assets โ”‚ โ”œโ”€โ”€ index.html # Server landing page (with template variables) โ”‚ โ””โ”€โ”€ favicon.ico # Site icon โ”‚ โ”œโ”€โ”€ installer/ # WiX MSI installer (Windows) โ”‚ โ”œโ”€โ”€ Product.wxs.in # WiX installer definition template โ”‚ โ”œโ”€โ”€ installer_banner_wix.bmp # Left-side banner (493ร—312) โ”‚ โ””โ”€โ”€ top_banner.bmp # Top banner with lemon icon (493ร—58) โ”‚ โ”œโ”€โ”€ server/ # Server implementation โ”‚ โ”œโ”€โ”€ main.cpp # Entry point, CLI routing โ”‚ โ”œโ”€โ”€ server.cpp # HTTP server (cpp-httplib) โ”‚ โ”œโ”€โ”€ router.cpp # Routes requests to backends โ”‚ โ”œโ”€โ”€ model_manager.cpp # Model registry, downloads, caching โ”‚ โ”œโ”€โ”€ cli_parser.cpp # Command-line argument parsing (CLI11) โ”‚ โ”œโ”€โ”€ recipe_options.cpp # Recipe option handling โ”‚ โ”œโ”€โ”€ wrapped_server.cpp # Base class for backend wrappers โ”‚ โ”œโ”€โ”€ streaming_proxy.cpp # Server-Sent Events for streaming โ”‚ โ”œโ”€โ”€ system_info.cpp # NPU/GPU device detection โ”‚ โ”œโ”€โ”€ lemonade.manifest.in # Windows manifest template โ”‚ โ”œโ”€โ”€ version.rc # Windows version resource โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ backends/ # Model backend implementations โ”‚ โ”‚ โ”œโ”€โ”€ backend_utils.cpp # Shared backend utilities โ”‚ โ”‚ โ”œโ”€โ”€ llamacpp_server.cpp # Wraps llama.cpp for LLM inference (CPU/GPU) โ”‚ โ”‚ โ”œโ”€โ”€ fastflowlm_server.cpp # Wraps FastFlowLM for NPU inference โ”‚ โ”‚ โ”œโ”€โ”€ ryzenaiserver.cpp # Wraps RyzenAI server for hybrid NPU โ”‚ โ”‚ โ”œโ”€โ”€ sd_server.cpp # Wraps Stable Diffusion for image generation โ”‚ โ”‚ โ””โ”€โ”€ whisper_server.cpp # Wraps whisper.cpp for audio transcription (CPU/NPU) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ utils/ # Utility functions โ”‚ โ”œโ”€โ”€ http_client.cpp # HTTP client using libcurl โ”‚ โ”œโ”€โ”€ json_utils.cpp # JSON file I/O โ”‚ โ”œโ”€โ”€ process_manager.cpp # Cross-platform process management โ”‚ โ”œโ”€โ”€ path_utils.cpp # Path manipulation โ”‚ โ”œโ”€โ”€ wmi_helper.cpp # Windows WMI for NPU detection โ”‚ โ””โ”€โ”€ wmi_helper.h # WMI helper header โ”‚ โ”œโ”€โ”€ include/lemon/ # Public headers โ”‚ โ”œโ”€โ”€ server.h # HTTP server interface โ”‚ โ”œโ”€โ”€ router.h # Request routing โ”‚ โ”œโ”€โ”€ model_manager.h # Model management โ”‚ โ”œโ”€โ”€ cli_parser.h # CLI argument parsing โ”‚ โ”œโ”€โ”€ recipe_options.h # Recipe option definitions โ”‚ โ”œโ”€โ”€ wrapped_server.h # Backend wrapper base class โ”‚ โ”œโ”€โ”€ streaming_proxy.h # Streaming proxy โ”‚ โ”œโ”€โ”€ system_info.h # System information โ”‚ โ”œโ”€โ”€ model_types.h # Model type definitions โ”‚ โ”œโ”€โ”€ audio_types.h # Audio type definitions โ”‚ โ”œโ”€โ”€ error_types.h # Error type definitions โ”‚ โ”œโ”€โ”€ server_capabilities.h # Server capability definitions โ”‚ โ”œโ”€โ”€ single_instance.h # Single instance enforcement โ”‚ โ”œโ”€โ”€ version.h.in # Version header template โ”‚ โ”œโ”€โ”€ backends/ # Backend headers โ”‚ โ”‚ โ”œโ”€โ”€ backend_utils.h # Backend utilities โ”‚ โ”‚ โ”œโ”€โ”€ llamacpp_server.h # LlamaCpp backend โ”‚ โ”‚ โ”œโ”€โ”€ fastflowlm_server.h # FastFlowLM backend โ”‚ โ”‚ โ”œโ”€โ”€ ryzenaiserver.h # RyzenAI backend โ”‚ โ”‚ โ”œโ”€โ”€ sd_server.h # Stable Diffusion backend โ”‚ โ”‚ โ””โ”€โ”€ whisper_server.h # Whisper backend โ”‚ โ””โ”€โ”€ utils/ # Utility headers โ”‚ โ”œโ”€โ”€ http_client.h # HTTP client โ”‚ โ”œโ”€โ”€ json_utils.h # JSON utilities โ”‚ โ”œโ”€โ”€ process_manager.h # Process management โ”‚ |โ”€โ”€ path_utils.h # Path utilities | |โ”€โ”€ network_beacon.h # Helps broadcast a beacon on port 13305 to network multicast โ”‚ โ””โ”€โ”€ tray/ # System tray application โ”œโ”€โ”€ CMakeLists.txt # Tray-specific build config โ”œโ”€โ”€ main.cpp # Entry point (WinMain on Windows, main on macOS/Linux) โ”œโ”€โ”€ tray_ui.h # TrayUI class header โ”œโ”€โ”€ tray_ui.cpp # TrayUI class โ€” menu, HTTP, icon, app launch (~500 lines) โ”œโ”€โ”€ agent_launcher.cpp # Agent (claude/codex) launcher (shared with CLI) โ”œโ”€โ”€ version.rc # Windows version resource โ””โ”€โ”€ platform/ # Platform-specific implementations โ”œโ”€โ”€ windows_tray.cpp # Win32 system tray API โ”œโ”€โ”€ macos_tray.mm # Objective-C++ NSStatusBar โ”œโ”€โ”€ linux_tray.cpp # GTK/AppIndicator โ””โ”€โ”€ tray_factory.cpp # Platform detection ``` ## Architecture Overview ### Overview The Lemonade Server C++ implementation uses a client-server architecture: #### lemond (Server Component) A pure HTTP server that: - Serves OpenAI-compatible REST API endpoints (supports both `/api/v0` and `/api/v1`) - Routes requests to appropriate LLM backends (llamacpp, fastflowlm, ryzenai) - Manages model loading/unloading and backend processes - Supports loading multiple models simultaneously with LRU eviction - Handles all inference requests - No command-based user interface - only accepts startup options **Key Layers:** - **HTTP Layer:** Uses cpp-httplib for HTTP server - **Router:** Determines which backend handles each request based on model recipe, manages multiple WrappedServer instances with LRU cache - **Model Manager:** Handles model discovery, downloads, and registry management - **Backend Wrappers:** Manages llama.cpp, FastFlowLM, RyzenAI, and whisper.cpp backends **Multi-Model Support:** - Router maintains multiple WrappedServer instances simultaneously - Separate LRU caches for LLM, embedding, reranking, and audio model types - NPU exclusivity: only one model can use NPU at a time - Configurable limits via `--max-loaded-models N` (default: 1) - Automatic eviction of least-recently-used models when limits reached - Thread-safe model loading with serialization to prevent races - Protection against evicting models actively serving inference requests #### lemonade (CLI Client) A console application for terminal users: - Provides command-based user interface (`list`, `pull`, `delete`, `run`, `status`, `logs`, `launch`, `backends`, `scan`) - Communicates with `lemond` via HTTP endpoints - Expects the server to already be running (auto-started by the OS after installation) #### lemonade-tray / LemonadeServer.exe (GUI Tray Application) A GUI application for desktop users that exposes the server via a system tray icon: - **Windows:** `LemonadeServer.exe` โ€” a SUBSYSTEM:WINDOWS app that embeds the server and shows a system tray icon. No console window. - **Linux:** `lemonade-tray` โ€” tray application (requires GTK3 + AppIndicator3). Connects to an already-running server if one is found; otherwise starts one (via systemd if a unit is installed, or by spawning `lemond` directly). - Zero console output or CLI interface - Used by application launchers, desktop shortcuts, and autostart entries - Provides seamless GUI experience for non-technical users ### Client-Server Communication The `lemonade` client communicates with `lemond` server via HTTP: - **Model operations:** `/api/v1/models`, `/api/v1/pull`, `/api/v1/delete` - **Model control:** `/api/v1/load`, `/api/v1/unload` - **Server management:** `/api/v1/health`, `/internal/shutdown`, `/internal/set`, `/internal/config`, `/internal/cleanup-cache` - **Inference:** `/api/v1/chat/completions`, `/api/v1/completions`, `/api/v1/audio/transcriptions` The client automatically: - Discovers the running server's port - Reports an error if no server is reachable **Single-Instance Protection:** - **Windows:** `LemonadeServer.exe` holds a system-wide mutex (`Global\LemondMutex`). A second launch shows a "Server is already running" dialog and exits. - **Linux/macOS:** `lemonade-tray` acquires an exclusive `flock()` on a lock file in the runtime directory to prevent duplicate tray instances. **Server Discovery:** - The `lemonade` CLI auto-discovers the running server via UDP beacon broadcast, falling back to the default port if no beacon is found. **Network Beacon based broadcasting:** - Uses port 13305 to broadcast to the network that it exists - Clients can read the json broadcast message to add server to server picker. - Uses machine hostname as broadcast name. - The custom flag --no-broadcast is available in the command line to disable. - Auto protection, doesnt broadcast on non RFC1918 Networks. ### Internal Endpoints > **These endpoints are for first-party Lemonade software only** (CLI, tray app, desktop app). They are not part of the public API, may change without notice, and must not be relied upon by third-party integrations. Internal endpoints are restricted to loopback (`127.0.0.1` / `::1`) โ€” requests from non-localhost addresses receive `403 Forbidden`. | Method | Path | Description | |--------|------|-------------| | `POST` | `/internal/shutdown` | Unloads all models and shuts down the server | | `POST` | `/internal/set` | Unified config setter (see below) | | `GET` | `/internal/config` | Returns the full runtime config snapshot | | `POST` | `/internal/cleanup-cache` | Cleans up orphaned files in the Hugging Face cache | #### `POST /internal/set` Accepts a JSON object with one or more keys to update atomically. Returns `{"status":"success","updated":{...}}` on success, or `400` with an error message on validation failure. **Server-level keys** (trigger immediate side effects): | Key | Type | Side Effect | |-----|------|-------------| | `port` | int (1โ€“65535) | HTTP rebind | | `host` | string | HTTP rebind | | `log_level` | string (`trace`, `debug`, `info`, `warning`, `error`, `fatal`, `none`) | Reconfigures log filter | | `global_timeout` | int (positive) | Updates default HTTP client timeout | | `no_broadcast` | bool | Stops or starts UDP beacon | | `extra_models_dir` | string | Updates model manager search path | **Deferred keys** (affect the next model load or eviction decision, no immediate side effect): | Key | Type | |-----|------| | `max_loaded_models` | int (-1 or positive) | | `ctx_size` | int (positive) | | `llamacpp_backend` | string | | `llamacpp_args` | string | | `sdcpp_backend` | string | | `whispercpp_backend` | string | | `whispercpp_args` | string | | `steps` | int (positive) | | `cfg_scale` | number | | `width` | int (positive) | | `height` | int (positive) | | `flm_args` | string | **Example:** ```bash curl -X POST http://localhost:13305/internal/set \ -H "Content-Type: application/json" \ -d '{"ctx_size": 8192, "max_loaded_models": 3, "log_level": "debug"}' ``` #### `GET /internal/config` Returns the full runtime configuration as a flat JSON object containing all server-level and recipe option keys with their current values. **Example:** ```bash curl http://localhost:13305/internal/config ``` ### Dependencies All dependencies are automatically fetched by CMake via FetchContent: - **cpp-httplib** (v0.26.0) - HTTP server with thread pool support [MIT License] - **nlohmann/json** (v3.11.3) - JSON parsing and serialization [MIT License] - **CLI11** (v2.4.2) - Command-line argument parsing [BSD 3-Clause] - **libcurl** (8.5.0) - HTTP client for model downloads [curl license] - **zstd** (v1.5.7) - Compression library for HTTP [BSD License] Platform-specific SSL backends are used (Schannel on Windows, SecureTransport on macOS, OpenSSL on Linux). ## Usage ### lemond (Server Only) The `lemond` executable is a pure HTTP server without any command-based interface: ```bash # Start server with default options ./lemond # Start server with custom port ./lemond --port 8080 # Available options: # [cache_dir] Path to lemonade cache directory (optional) # --port PORT Port number (default: 13305) # --host HOST Bind address (default: localhost) # --version, -v Show version # --help, -h Show help ``` All other server settings are managed via `lemonade config set` (see [Server Configuration](./server/configuration.md)). ### lemonade (CLI Client) The `lemonade` executable is the command-line interface for terminal users: - Command-line interface for model management and server interaction - Communicates with `lemond` via HTTP endpoints - Expects the server to already be running (auto-started by the OS after installation) ```bash # List available models ./lemonade list # Pull a model ./lemonade pull Llama-3.2-1B-Instruct-CPU # Delete a model ./lemonade delete Llama-3.2-1B-Instruct-CPU # Check server status ./lemonade status # Run a model (loads model and opens browser) ./lemonade run Llama-3.2-1B-Instruct-CPU # View server logs ./lemonade logs # List recipes and backends ./lemonade backends ``` ### LemonadeServer.exe / lemonade-tray (GUI Tray Application) The tray application provides a system tray icon for desktop users: - Double-click from Start Menu, application launcher, or Desktop to start server - Zero console windows or CLI interface โ€” always starts the tray directly - Perfect for non-technical users - Single-instance protection: shows friendly message if already running **Platform support:** - **Windows:** `LemonadeServer.exe` โ€” a SUBSYSTEM:WINDOWS app that embeds `lemond` and shows a system tray icon. No separate console process. Auto-starts via the Windows startup folder. - **Linux:** `lemonade-tray` โ€” available when compiled with GTK3 + AppIndicator3 support (auto-detected at build time). Connects to an already-running server if one is found; otherwise starts one (via systemd if a unit is installed, or by spawning `lemond` directly). **What it does (Linux):** 1. Starts immediately in tray mode (no subcommand needed) 2. Connects to an already-running server, or starts one (via systemd if a unit is installed, otherwise spawns `lemond` directly) 3. Shows a system tray icon connected to the server **When to use:** - Launching from Start Menu (Windows) or application launcher (Linux) - Desktop shortcuts - Windows startup / Linux autostart - Any GUI/point-and-click scenario **System Tray Features (when running):** - Left-click or right-click icon to show menu - Load/unload models via menu - Change server port and context size - Open web UI, documentation, and logs - "Show Logs" opens the desktop app's logs view with historical and live logs - Background model monitoring - Click balloon notifications to open menu - Quit option **UI Improvements:** - Displays as "Lemonade Local LLM Server" in Task Manager - Shows large lemon icon in notification balloons - Single-instance protection prevents multiple tray apps ### Logging and Console Output When running `LemonadeServer.exe` or `lemond`: - **Log File:** Direct runs write logs to a persistent log file (default: `%TEMP%\lemonade-server.log` on Windows). When `lemond` runs as the systemd service, logs go to the journal instead. - **Logs UI:** Click "Show Logs" in the tray or use `lemonade logs` to open the desktop app's logs view - Connects to the server's WebSocket log stream - Shows retained recent log history plus live entries - Reconnects automatically if the stream drops **Logs UI Features:** - Real-time streaming over `/logs/stream` - Snapshot + live log entries - Integrated into the desktop app instead of a standalone log viewer binary ## Testing ### Basic Functionality Tests Run the commands from the Usage section above to verify basic functionality. ### Integration Tests The C++ implementation is tested using the existing Python test suite. **Prerequisites:** - Python 3.10+ - Test dependencies: `pip install -r test/requirements.txt` **Python integration tests** (from `test/` directory, ordered least to most complex): | Test File | Description | |-----------|-------------| | `server_cli.py` | CLI commands (version, list, pull, status, delete, serve, stop, run) | | `server_endpoints.py` | HTTP endpoints (health, models, pull, load, unload, system-info, stats) | | `server_llm.py` | LLM inference (chat completions, embeddings, reranking) | | `server_whisper.py` | Audio transcription (whisper models) | | `server_sd.py` | Image generation (Stable Diffusion, ~2-3 min per image on CPU) | **Running tests:** ```bash # CLI tests (no inference backend needed) python test/server_cli.py # Endpoint tests (no inference backend needed) python test/server_endpoints.py # LLM tests (specify wrapped server and backend) python test/server_llm.py --wrapped-server llamacpp --backend vulkan # Audio transcription tests python test/server_whisper.py # Image generation tests (slow) python test/server_sd.py ``` The tests auto-discover the server binary from the build directory. Use `--server-binary` to override if needed. See the `.github/workflows/` directory for CI/CD test configurations. **Note:** The Python tests should now use `lemonade-server.exe` as the entry point since it provides the CLI interface. ## Development ### Code Style - C++17 standard - Snake_case for functions and variables - CamelCase for classes and types - 4-space indentation - Header guards using `#pragma once` - All code in `lemon::` namespace ### Key Resources - **API Specification:** `docs/server/server_spec.md` - **Model Registry:** `src/cpp/resources/server_models.json` - **Web UI Files:** `src/cpp/resources/static/` - **Backend Versions:** `src/cpp/resources/backend_versions.json` ## License This project is licensed under the Apache 2.0 License. All dependencies use permissive licenses (MIT, BSD, Apache 2.0, curl license). lemonade-sdk-lemonade-dbde812/docs/driver_install.html000066400000000000000000000177221516551144000232040ustar00rootroot00000000000000 NPU Driver Installation - Lemonade Server
NPU Driver Installation
Install the required NPU driver for AMD Ryzen AI processors

Why do I need the NPU driver?

Lemonade uses Ryzen AI Software and FastFlowLM software to access the NPU (Neural Processing Unit) on AMD Ryzen AI processors. New releases of these engines often require an updated NPU driver. The NPU driver is required for AI acceleration and enables hardware-accelerated inference for machine learning models on compatible systems.

Note: The minimum required NPU driver version for RyzenAI 1.6.0 is 32.0.203.280 and the FastFlowLM recommended driver version is provided here.

Installation Options

Option A Recommended

Use AMD's Driver Download Tool

Visit AMD's official driver download page to automatically detect and install the correct NPU driver for your system:

Go to AMD Driver Downloads

Follow the on-screen instructions to detect your hardware and download the appropriate driver.

Option B If Option A doesn't work

Manual Driver Installation

If Option A doesn't work or you need a specific driver version, follow these manual steps:

1

Download the NPU Driver

Click the button below to download the NPU driver directly:

Download NPU Driver
2

Install the Driver

Unzip the downloaded folder. Then, open an Administrator Command Prompt, navigate to the unzipped folder, and run npu_sw_installer.exe. Follow the installation wizard to complete the process.

3

Verify Installation

Try Lemonade Server again to verify the NPU driver is working correctly.

Need Help?

If you continue to experience issues after following these steps:

lemonade-sdk-lemonade-dbde812/docs/embeddable/000077500000000000000000000000001516551144000213305ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/docs/embeddable/README.md000066400000000000000000000143521516551144000226140ustar00rootroot00000000000000# Embeddable Lemonade Guide Embeddable Lemonade is a portable build of the `lemond` service that you can bundle into your app. Contents: - [Who is this for?](#who-is-this-for) - [What's in the release artifact?](#whats-in-the-release-artifact) - [Customization Overview](#customization-overview) - [How it Works](#how-it-works) - [Deployment-Ready Layout](#deployment-ready-layout) - [In-Depth Customization](#in-depth-customization) ## Who is this for? Use Embeddable Lemonade instead of a global Lemonade Service when you want a cohesive end-to-end experience for users of your app. - Users only see your installer, icons, etc. - Prevent users and other apps from directly interacting with `lemond`. - Keep your models private from the rest of the system. - Customize `lemond` to your exact specifications, including backend versions, available models, and much more. ## What's in the release artifact? Embeddable Lemonade is an zip/tarball artifact shipped in Lemonade releases. - Windows: `lemonade-embeddable-10.1.0-windows-x64.zip` - Ubuntu: `lemonade-embeddable-10.1.0-ubuntu-x64.tar.gz` > Note: see the [Building from Source](./building.md) for instructions for building your own embeddable Lemonade from source, including for other Linux distros. Each archive has the following contents: - `lemond.exe` / `lemond` executable: your own private Lemonade instance. - `lemonade.exe` / `lemonade` CLI: useful for configuring and testing `lemond` before you ship. Feel free to exclude this from your shipped app. - `resources/` - `server_models.json`: customizable list of models that `lemond` will show on the `models` endpoint. - `backend_versions.json`: customizable list that determines which versions of llama.cpp, FastFlowLM, etc. will be used as backends for `lemond`. - `defaults.json`: default values for `lemond`'s `config.json` file. Safe to delete after `config.json` has been initialized. ## Customization Overview While you can ship Embeddable Lemonade as-is, there many opportunities to customize it before packaging it into your app. ### How it Works Many of the customization options rely of `lemond`'s `config.json` file, a persistent store of settings. Learn more about the individual settings in the [configuration guide](../server/configuration.md). `config.json` is automatically generated based on the values in `resources/defaults.json` the first time `lemond` starts. The positional arg `lemond DIR` determines where `config.json` and other runtime files (e.g., backend binaries) will be located. In the examples in this guide, we start `lemond ./` to place these files in the same directory as `lemond` itself. Then: 1. We use the `lemonade` CLI's `config set` command to programmatically customize the contents of `config.json` (you can also manually edit `config.json` if you prefer). 2. Use `lemonade backends install` to pre-download backends to be bundled in your app. 3. Edit `server_models.json` and `backend_versions.json` to fully customize the experience for your users. 4. You can delete the `lemonade` CLI and `defaults.json` files to minimize the footprint of your app. Finally, you can place the fully-configured Embeddable Lemonade folder into your app's installer. ### Deployment-Ready Layout Once you've finished customization, you'll have a portable Lemonade folder ready for deployment with a layout like this: === "Windows (cmd.exe)" ```text lemond.exe # App runs lemond as a subprocess lemonade.exe # Optional: CLI management for lemond LICENSE # Lemonade license file config.json # Persistent customized settings for lemond recipe_options.json # Per-model customization (e.g., llama args) resources\ |- server_models.json # Customized lemond models list |- backend_versions.json # Customized version numbers for llamacpp, etc. bin\ # Pre-downloaded backends bundled into app |- llamacpp\ # GPU LLMs, embedding, and reranking |- rocm\ |- llama-server.exe |- vulkan\ |- llama-server.exe |- ryzenai-server\ # NPU LLMs |- flm\ # NPU LLMs, embedding, and ASR |- sdpp\ # GPU image generation |- whispercpp\ # NPU and GPU ASR models\ # Hugging Face standard layout for models |- models--unsloth--Qwen3-0.6B-GGUF\ extra_models\ # Additional GGUF files |- my_custom_model.gguf ``` === "Linux (bash)" ```text lemond # App runs lemond as a subprocess lemonade # Optional: CLI management for lemond LICENSE # Lemonade license file config.json # Persistent customized settings for lemond recipe_options.json # Per-model customization (e.g., llama args) resources/ |- server_models.json # Customized lemond models list |- backend_versions.json # Customized version numbers for llamacpp, etc. bin/ # Pre-downloaded backends bundled into app |- llamacpp/ # GPU LLMs, embedding, and reranking |- rocm/ |- llama-server |- vulkan/ |- llama-server |- ryzenai-server/ # NPU LLMs |- flm/ # NPU LLMs, embedding, and ASR |- sdpp/ # GPU image generation |- whispercpp/ # NPU and GPU ASR models/ # Hugging Face standard layout for models |- models--unsloth--Qwen3-0.6B-GGUF/ extra_models/ # Additional GGUF files |- my_custom_model.gguf ``` ## In-Depth Customization Reference detailed guides for each of the following subjects: - [Runtime](./runtime.md): Using `lemond` as a subprocess runtime. - [Backends](./backends.md): Deploy backends at packaging time, install time, or runtime. - [Models](./models.md): Bundling, organization, sharing, per-model settings. - [Building from Source](./building.md): Customize `lemond` compile-time features. lemonade-sdk-lemonade-dbde812/docs/embeddable/backends.md000066400000000000000000000137651516551144000234400ustar00rootroot00000000000000# Embeddable Lemonade: Backends This guide discusses how to set up and manage backends for `lemond`. Backends are the software that implements inference, such as `llama.cpp`, `whisper.cpp`, `FastFlowLM`, etc. `lemond` can install backends on your behalf, or it can utilize backends that are already part of your app. You can also download backends at packaging time, install time, or runtime. Contents: - [Setting Up Lemonade's Backends](#setting-up-lemonades-backends) - [Customizing Backend Versions](#customizing-backend-versions) - [Bundling Backends at Packaging Time](#bundling-backends-at-packaging-time) - [Installing Backends at Install-Time or Runtime](#installing-backends-at-install-time-or-runtime) - [Bring Your Own Backends](#bring-your-own-backends) ## Setting Up Lemonade's Backends ### Customizing Backend Versions Each version of `lemond` ships with recommended version numbers for each support backend, which can be found in `resources/backend_versions.json`. For example, `lemond v10.0.1` recommends `ggml-org/llama.cpp` version `b8460`, `FastFlowLM v0.9.36`, etc. These backend versions have been validated against that specific release of `lemond` to ensure compatibility, and represent a good starting point for you app. However, you can also customize `backend_versions.json` to your requirements. If you change any backend version, simply restart `lemond` and run any install, load, or inference request against that backend to trigger the new backend version to install. ### Bundling Backends at Packaging Time Follow these instructions if you want backends to be bundled into your app's installer: 1. Start `lemond ./` on the system where you are packaging your app. 2. Run `lemonade backends` to see the full set of supported backends. 3. `lemonade backends install BACKEND:DEVICE` for each backend. === "Windows (cmd.exe)" ```cmd REM Start lemond to download backends to ./bin/ lemond.exe ./ REM Download llama.cpp with the Vulkan backend to ./bin/llamacpp/vulkan lemonade.exe backends install llamacpp:vulkan ``` === "Linux (bash)" ```bash # Start lemond to download backends to ./bin/ ./lemond ./ # Download llama.cpp with the Vulkan backend to ./bin/llamacpp/vulkan ./lemonade backends install llamacpp:vulkan --force ``` > Note: by default, `lemond backends install` will only install backends that are compatible with your current system. The `--force` option ignores these compatibility checks, which enables you to package on a VM and then deploy to a specific system. #### Limitations At the time of this writing: - `flm` is not available for packaging-time bundling *on Linux*. - `llamacpp:rocm` is not available for packaging-time bundling on any OS. ### Installing Backends at Install-Time or Runtime You can install backends either during your app's installer or first-run flow, or later while the app is running. In both cases, start by calling [`GET /v1/system-info`](../server/server_spec.md#get-apiv1system-info) on the target machine. The response tells you which backends are supported on that specific system. This is useful when the correct backend depends on the user's hardware. For example, you can prefer `llamacpp:rocm` when ROCm is supported, and fall back to `llamacpp:vulkan` otherwise. Example flow: 1. Launch `lemond`. 2. Call `/v1/system-info`. 3. Check `recipes.llamacpp.backends.rocm.devices` or `recipes.llamacpp.backends.rocm.state`. 4. If ROCm is supported, call `POST /v1/install` with `{"recipe":"llamacpp","backend":"rocm"}`. 5. Otherwise, call `POST /v1/install` with `{"recipe":"llamacpp","backend":"vulkan"}`. For example: === "Windows (cmd.exe)" ```cmd curl http://localhost:8000/v1/system-info ``` === "Linux (bash)" ```bash curl http://localhost:8000/v1/system-info ``` If the response shows ROCm support: ```json { "recipes": { "llamacpp": { "backends": { "rocm": { "devices": ["amd_igpu"], "state": "installable" } } } } } ``` Install ROCm: === "Windows (cmd.exe)" ```cmd curl -X POST http://localhost:8000/v1/install ^ -H "Content-Type: application/json" ^ -d "{\"recipe\": \"llamacpp\", \"backend\": \"rocm\", \"stream\": false}" ``` === "Linux (bash)" ```bash curl -X POST http://localhost:8000/v1/install \ -H "Content-Type: application/json" \ -d '{ "recipe": "llamacpp", "backend": "rocm", "stream": false }' ``` Otherwise, install Vulkan: === "Windows (cmd.exe)" ```cmd curl -X POST http://localhost:8000/v1/install ^ -H "Content-Type: application/json" ^ -d "{\"recipe\": \"llamacpp\", \"backend\": \"vulkan\", \"stream\": false}" ``` === "Linux (bash)" ```bash curl -X POST http://localhost:8000/v1/install \ -H "Content-Type: application/json" \ -d '{ "recipe": "llamacpp", "backend": "vulkan", "stream": false }' ``` See the [Server Spec](../server/server_spec.md) for endpoint details. ## Bring Your Own Backends You can provide `lemond` the path to your own backend binaries with the following settings. This will cause `lemond` to use your custom backend binaries instead of downloading its own. This is useful if you have a highly customized backend binary you want to use, or if you want to share backend binaries between `lemond` and other software in your application. For example, to use your own Vulkan `llama-server` in place of Lemonade's: === "Windows (cmd.exe)" ```cmd REM Start lemond to update configuration lemond.exe ./ REM Set the llama-server vulkan binary path lemonade.exe config set llamacpp.vulkan_bin C:\path\to\bins ``` === "Linux (bash)" ```bash # Start lemond to update configuration ./lemond ./ # Set the llama-server vulkan binary path ./lemonade config set llamacpp.vulkan_bin /path/to/bins ``` See the `*_bin` settings in the [Configuration Guide](../server/configuration.md) for the full set of customization options. lemonade-sdk-lemonade-dbde812/docs/embeddable/building.md000066400000000000000000000043201516551144000234460ustar00rootroot00000000000000# Embeddable Lemonade: Building from Source This guide shows how to build the embeddable `lemond` and `lemonade` binaries from source. For general prerequisites, toolchain setup, and broader development workflows, see [Lemonade Development](../dev-getting-started.md). Contents: - [Default Embeddable Build](#default-embeddable-build) - [Include the Web App](#include-the-web-app) - [Expected Outputs](#expected-outputs) ## Default Embeddable Build The `embeddable` CMake target builds the server, CLI, and required resource files, then packages them into a single archive. The [release workflow](../../.github/workflows/cpp_server_build_test_release.yml) uses this target to produce the embeddable archives. === "Windows (cmd.exe)" ```cmd cmake --preset windows -DBUILD_WEB_APP=OFF cmake --build --preset windows --target embeddable ``` This produces `build\lemonade-embeddable-{VERSION}-windows-x64.zip`. === "Linux (bash)" ```bash sudo apt-get update sudo apt-get install -y cmake ninja-build g++ pkg-config libssl-dev libdrm-dev cmake --preset default -DBUILD_WEB_APP=OFF cmake --build --preset default --target embeddable ``` This produces `build/lemonade-embeddable-{VERSION}-ubuntu-x64.tar.gz`. ## Include the Web App If you want the embeddable build to include the browser UI assets under `resources/web-app`, enable `BUILD_WEB_APP` and build the `web-app` target before `embeddable`: === "Windows (cmd.exe)" ```cmd cmake --preset windows -DBUILD_WEB_APP=ON cmake --build --preset windows --target web-app embeddable ``` === "Linux (bash)" ```bash cmake --preset default -DBUILD_WEB_APP=ON cmake --build --preset default --target web-app embeddable ``` ## Expected Outputs The `embeddable` target produces a single archive in `build/`: | Platform | Archive | |----------|---------| | Linux | `lemonade-embeddable-{VERSION}-ubuntu-x64.tar.gz` | | Windows | `lemonade-embeddable-{VERSION}-windows-x64.zip` | Each archive contains: - `lemond` (or `lemond.exe`) โ€” the server binary - `lemonade` (or `lemonade.exe`) โ€” the CLI binary - `LICENSE` - `resources/server_models.json` - `resources/backend_versions.json` - `resources/defaults.json` lemonade-sdk-lemonade-dbde812/docs/embeddable/models.md000066400000000000000000000152021516551144000231350ustar00rootroot00000000000000# Embeddable Lemonade: Models This guide covers how `lemond` discovers, exposes, and bundles models in an embeddable deployment. Contents: - [Model Organization](#model-organization) - [Sharing Models With Other Apps](#sharing-models-with-other-apps) - [Private Models](#private-models) - [Importing Models to `lemond`](#importing-models-to-lemond) - [Customization](#customization) - [Changing the Built-In Models List](#changing-the-built-in-models-list) - [Per-Model Load Options](#per-model-load-options) - [Bundling Models](#bundling-models) ## Model Organization `lemond`'s configuration has two properties, `models_dir` and `extra_models_dir`, that determine where `lemond` will look when listing, pulling, and loading models. - `models_dir` is the primary model store, where `lemond` will `pull` models to. - `extra_models_dir` is a search path for GGUF LLMs that can be imported into `lemond`. ### Sharing Models With Other Apps The default value for `models_dir` is `"auto"`, which means "respect my user's `HF_HOME` and `HF_HUB_CACHE` environment variables, in accordance with the Hugging Face Hub standard." If those environment variables are not set, it defaults to `~/.cache/huggingface/hub`. The benefit of `models_dir="auto"` is that models are placed in a common location on your user's drive, which means they can be shared across applications. However, the downside is that other applications could manage models that your app relies on. ### Private Models You can keep models completely private to your `lemond` instance by changing `models_dir` to be inside of the `lemond` directory: === "Windows (cmd.exe)" ```cmd REM Start lemond for configuration lemond.exe ./ REM Set models_dir to be inside the lemond directory lemonade.exe config set models_dir="./models" ``` === "Linux (bash)" ```bash # Start lemond for configuration ./lemond ./ # Set models_dir to be inside the lemond directory ./lemonade config set models_dir="./models" ``` You can test whether models end up at the desired location with a pull command: === "Windows (cmd.exe)" ```cmd lemonade.exe pull Qwen3-0.6B-GGUF dir models ``` === "Linux (bash)" ```bash ./lemonade pull Qwen3-0.6B-GGUF ls models ``` Which should return: ``` models--unsloth--Qwen3-0.6B-GGUF ``` > Note: if you need to use a GGUF LLM that is not available on huggingface.co, you can use GGUF files acquired through other means using the `extra_models_dir` configuration discussed in [Importing Models to `lemond`](#importing-models-to-lemond). ### Importing Models to `lemond` You may want to share models within your app if `lemond` is not your only inference provider for GGUF LLMs. There is also a scenario where `lemond`'s Hugging Face Hub-based `pull` methodology is not able to obtain the models you need, for example: - Hugging Face Hub is blocked in your users' locality. - Your GGUF files are not on Hugging Face Hub. To address all of these scenarios, `lemond` can recursively search a directory for GGUF files using the `extra_models_dir` configuration. For example, let's copy the GGUF file from the [Private Models](#private-models) example to another folder so that we have an arbitrary GGUF LLM to work with: === "Windows (cmd.exe)" ```cmd mkdir example_extra_models_dir copy models\models--unsloth--Qwen3-0.6B-GGUF\snapshots\50968a4468ef4233ed78cd7c3de230dd1d61a56b\Qwen3-0.6B-Q4_0.gguf example_extra_models_dir\my_custom_model.gguf ``` === "Linux (bash)" ```bash mkdir example_extra_models_dir cp models/models--unsloth--Qwen3-0.6B-GGUF/snapshots/50968a4468ef4233ed78cd7c3de230dd1d61a56b/Qwen3-0.6B-Q4_0.gguf example_extra_models_dir/my_custom_model.gguf ``` Now we have a GGUF, `my_custom_model.gguf`, in a non-standard directory layout with a non-standard name. To import it into `lemond`: === "Windows (cmd.exe)" ```cmd REM Start lemond for configuration lemond.exe ./ REM Set the extra_models_dir search path lemonade.exe config set extra_models_dir="./example_extra_models_dir" ``` === "Linux (bash)" ```bash # Start lemond for configuration ./lemond ./ # Set the extra_models_dir search path ./lemonade config set extra_models_dir="./example_extra_models_dir" ``` You can test whether it worked like this: === "Windows (cmd.exe)" ```cmd lemonade.exe list | findstr custom ``` === "Linux (bash)" ```bash ./lemonade list | grep custom ``` Which should return: ``` extra.my_custom_model.gguf Yes llamacpp ``` > Note: models imported via `extra_models_dir` will have the `extra.` prefix in the `list` command and `/v1/models` endpoint. > Tip: `extra_models_dir` can be a relative path to any location within your app's package, or any absolute path on your user's system. It searches recursively and can import many GGUFs from a single directory tree. ## Customization ### Changing the Built-In Models List `lemond` has a built-in list of over 100 suggested models in `resources/server_models.json`. This list determines the models that are available by name in the `/v1/models`, `/v/1/pull`, and `/v1/delete` endpoints at run time. You can print the list with: === "Windows (cmd.exe)" ```cmd lemond.exe ./ lemonade.exe list ``` === "Linux (bash)" ```bash ./lemond ./ ./lemonade list ``` You can customize this list at packaging time by editing `server_models.json`. You can remove any entry to make it inaccessible to your users, or you can add entries to introduce new options. > Note: `lemond` needs to be restarted for `server_models.json` changes to take effect. For example, if you wanted to add [OmniCoder-9B](https://huggingface.co/Tesslate/OmniCoder-9B-GGUF) to your application, add this entry to `server_models.json`: ``` "OmniCoder-9B-GGUF": { "checkpoint": "Tesslate/OmniCoder-9B-GGUF:omnicoder-9b-q4_k_m.gguf", "recipe": "llamacpp", "suggested": true, "labels": [ "tool-calling" ], "size": 5.74 } ``` You can learn more in the [Custom Models](../server/custom-models.md) guide, including how to enable your users to register their own custom models at runtime. ### Per-Model Load Options You may want to customize how specific models will load on your user's system. You can do this with the `recipe_options.json` file. Learn more in the [Custom Models](../server/custom-models.md) guide. ## Bundling Models You can leverage `lemond` to include model files in your app's installer. To do so, follow the same instructions as the [Private Models](#private-models) section, and then package the `models` folder into your app's installer. lemonade-sdk-lemonade-dbde812/docs/embeddable/runtime.md000066400000000000000000000132561516551144000233440ustar00rootroot00000000000000# Embeddable Lemonade: Runtime This guide will show you how to operate Embeddable Lemonade in your app at runtime. Contents: - [Launching](#launching) - [Authenticating Requests](#authenticating-requests) - [Runtime Model and Backend Management](#runtime-model-and-backend-management) - [Runtime Settings Management](#runtime-settings-management) - [`GET /internal/config`](#get-internalconfig) - [`POST /internal/set`](#post-internalset) ## Launching We recommend that your app launches `lemond` as a subprocess, using a command like this: === "Windows (cmd.exe)" ```cmd set LEMONADE_API_KEY=KEY && lemond.exe ./ --port PORT ``` === "Linux (bash)" ```bash LEMONADE_API_KEY=KEY lemond ./ --port PORT ``` Breaking this down: - `LEMONADE_API_KEY=KEY` sets an API key for `lemond` known only to your app. This locks out other apps, as well as users, from interfacing directly with `lemond`'s endpoints. - The positional `./` is the working directory for `lemond`, where it will look for `config.json`, `bin/`, etc. - `--port PORT` ensures that `lemond` launches on a specific port where your app will find it. ## Authenticating Requests If you launch `lemond` with `LEMONADE_API_KEY` set, your app must send that same key on every HTTP request to Lemonade endpoints. Do this by setting an `Authorization` header with a Bearer token: ```http Authorization: Bearer KEY ``` For example, with `curl`: === "Windows (cmd.exe)" ```cmd curl http://localhost:8000/v1/health ^ -H "Authorization: Bearer KEY" ``` === "Linux (bash)" ```bash curl http://localhost:8000/v1/health \ -H "Authorization: Bearer KEY" ``` For JSON `POST` requests: === "Windows (cmd.exe)" ```cmd curl -X POST http://localhost:8000/internal/set ^ -H "Authorization: Bearer KEY" ^ -H "Content-Type: application/json" ^ -d "{\"log_level\": \"debug\"}" ``` === "Linux (bash)" ```bash curl -X POST http://localhost:8000/internal/set \ -H "Authorization: Bearer KEY" \ -H "Content-Type: application/json" \ -d '{"log_level": "debug"}' ``` In JavaScript: ```js await fetch("http://localhost:8000/v1/models", { headers: { Authorization: `Bearer ${apiKey}`, }, }); ``` This matches the existing CLI, tray, app, and test implementations in this repo. If the header is missing or the key is wrong, `lemond` will reject the request with `401 Unauthorized`. ## Runtime Model and Backend Management `lemond` provides a full set of endpoints for managing models and backends at runtime. | Endpoint | Description | |----------|-------------| | `POST /v1/pull` | Download a model | | `POST /v1/delete` | Delete a downloaded model | | `POST /v1/load` | Load a model into memory | | `POST /v1/unload` | Unload a model from memory | | `POST /v1/install` | Install or update a backend | | `POST /v1/uninstall` | Remove a backend | | `GET /v1/models` | List available models | | `GET /v1/health` | Server status and loaded models | See the [Server Spec](../server/server_spec.md) for full request/response details. ## Runtime Settings Management Your app can manage its `lemond` instance at runtime by using `/internal` endpoints. | Method | Path | Description | |--------|------|-------------| | `POST` | `/internal/set` | Unified config setter (see below) | | `GET` | `/internal/config` | Returns the full runtime config snapshot | The settings defined in `config.json` can all be changed at runtime without restarting `lemond` with the `/internal/set` endpoint. See the [Configuration Guide](../server/configuration.md) for details on all settings. > Note: The `lemonade` CLI uses `/internal/set` and `/internal/config` internally for the `lemonade config` commands. #### `GET /internal/config` Returns the full runtime configuration as a flat JSON object containing all server-level and recipe option keys with their current values. **Example:** === "Windows (cmd.exe)" ```cmd curl http://localhost:8000/internal/config ``` === "Linux (bash)" ```bash curl http://localhost:8000/internal/config ``` #### `POST /internal/set` Accepts a JSON object with one or more keys to update atomically. Returns `{"status":"success","updated":{...}}` on success, or `400` with an error message on validation failure. **Server-level keys** (trigger immediate side effects): | Key | Type | Side Effect | |-----|------|-------------| | `port` | int (1โ€“65535) | HTTP rebind | | `host` | string | HTTP rebind | | `log_level` | string (`trace`, `debug`, `info`, `warning`, `error`, `fatal`, `none`) | Reconfigures log filter | | `global_timeout` | int (positive) | Updates default HTTP client timeout | | `no_broadcast` | bool | Stops or starts UDP beacon | | `extra_models_dir` | string | Updates model manager search path | **Deferred keys** (affect the next model load or eviction decision, no immediate side effect): | Key | Type | |-----|------| | `max_loaded_models` | int (-1 or positive) | | `ctx_size` | int (positive) | | `llamacpp_backend` | string | | `llamacpp_args` | string | | `sdcpp_backend` | string | | `whispercpp_backend` | string | | `whispercpp_args` | string | | `steps` | int (positive) | | `cfg_scale` | number | | `width` | int (positive) | | `height` | int (positive) | | `flm_args` | string | **Example:** === "Windows (cmd.exe)" ```cmd curl -X POST http://localhost:8000/internal/set ^ -H "Content-Type: application/json" ^ -d "{\"ctx_size\": 8192, \"max_loaded_models\": 3, \"log_level\": \"debug\"}" ``` === "Linux (bash)" ```bash curl -X POST http://localhost:8000/internal/set \ -H "Content-Type: application/json" \ -d '{"ctx_size": 8192, "max_loaded_models": 3, "log_level": "debug"}' ``` lemonade-sdk-lemonade-dbde812/docs/faq.md000066400000000000000000000332741516551144000203660ustar00rootroot00000000000000# ๐Ÿ‹ Lemonade Frequently Asked Questions ## Overview ### 1. **What is Lemonade and what does it include?** Lemonade is an open-source local LLM solution that: - Gets you started in minutes with one-click installers. - Auto-configures optimized inference engines for your PC. - Provides a convenient app to get set up and test out LLMs. - Provides LLMs through the OpenAI API standard, enabling apps on your PC to access them. ### 2. **What are the use cases for different audiences?** - **LLM Enthusiasts**: LLMs on your GPU or NPU with minimal setup, and connect to great apps listed [here](https://lemonade-server.ai/docs/server/apps/). - **Developers**: Integrate LLMs into apps using standard APIs with no device-specific code. See the [Server Integration Guide](https://lemonade-server.ai/docs/server/server_integration/ ). - **Agent Developers**: Use [GAIA](https://github.com/amd/gaia) to quickly develop local-first agents. ## Installation & Compatibility ### 1. **How do I install Lemonade SDK or Server?** Visit https://lemonade-server.ai/install_options.html and click the options that apply to you. ### 2. **Which devices are supported?** ๐Ÿ‘‰ [Supported Configurations](https://github.com/lemonade-sdk/lemonade?tab=readme-ov-file#supported-configurations) For more information on AMD Ryzen AI NPU Support, see the section [Hybrid/NPU](#hybrid-and-npu-questions). ### 3. **Is Linux supported? What about macOS?** Yes, both Linux and macOS are supported! - **Linux**: Visit https://lemonade-server.ai/install_options.html#linux for installation instructions. - **macOS (beta)**: A macOS installer (.pkg) is available for Apple Silicon Macs. Visit https://lemonade-server.ai/install_options.html#macos to download. macOS support uses the llama.cpp backend with Metal acceleration. Visit the [Supported Configurations](https://github.com/lemonade-sdk/lemonade?tab=readme-ov-file#supported-configurations) section to see the support matrix for CPU, GPU, and NPU. ### 4. **How do I uninstall Lemonade Server? (Windows)** To uninstall Lemonade Server, use the Windows Add/Remove Programs menu. **Optional: Remove cached files** - Open File Explorer and navigate to `%USERPROFILE%\.cache` - Delete the `lemonade` folder if it exists - To remove downloaded models, delete the `huggingface` folder ## Models ### 1. **Where are models stored and how do I change that?** Lemonade uses three model locations: **Primary: Hugging Face Cache** Models downloaded through Lemonade are stored using the Hugging Face Hub specification. By default, models are located at `~/.cache/huggingface/hub/`, where `~` is your home directory. For example, `Qwen/Qwen2.5-0.5B` is stored at `~/.cache/huggingface/hub/models--Qwen--Qwen2.5-0.5B`. You can change this location by setting the `HF_HOME` env var, which will store your models in `$HF_HOME/hub` (e.g., `$HF_HOME/hub/models--Qwen--Qwen2.5-0.5B`). Alternatively, you can set `HF_HUB_CACHE` and your models will be in `$HF_HUB_CACHE` (e.g., `$HF_HUB_CACHE/models--Qwen--Qwen2.5-0.5B`). You can use the official Hugging Face Hub utility (`pip install huggingface-hub`) to manage models outside of Lemonade, e.g., `hf cache ls` will print all models and their sizes. **Secondary: Extra Models Directory (GGUF)** Lemonade Server can discover GGUF models from a secondary directory using the `extra_models_dir` option, enabling compatibility with llama.cpp and LM Studio model caches. Suggested paths: - **Windows:** - LM Studio: `C:\Users\You\.lmstudio\models` - llamacpp: `%LOCALAPPDATA%\llama.cpp` (e.g., `C:\Users\You\AppData\Local\llama.cpp`) - **Linux:** `~/.cache/llama.cpp` Set `extra_models_dir` (see [Server Configuration](./server/configuration.md)): ```bash lemonade config set extra_models_dir="/home/you/.cache/llama.cpp" ``` Any `.gguf` files found in this directory (including subdirectories) will automatically appear in Lemonade's model list in the `custom` category. **FastFlowLM** FastFlowLM (FLM) has its own model management system. When you first install FLM the install wizard asks for a model directory, which is then saved to the `FLM_MODEL_PATH` environment variable on your system PATH. Models are stored in that directory. If you change the variable's value, newly downloaded models will be stored on the new path, but your prior models will still be at the prior path. ### 2. **What models are supported?** Lemonade supports a wide range of LLMs including LLaMA, DeepSeek, Qwen, Gemma, Phi, gpt-oss, LFM, and many more. Most GGUF models can also be added to Lemonade Server by users using the Model Manager interface in the app or the `pull` command on the CLI. ๐Ÿ‘‰ [Supported Models List](https://lemonade-server.ai/models.html) ๐Ÿ‘‰ [pull command](https://lemonade-server.ai/docs/lemonade-cli/#options-for-pull) ### 3. **How do I know what size model will work with my setup?** Model compatibility depends on your system's RAM, VRAM, and NPU availability. **The actual file size varies significantly between models** due to different quantization techniques and architectures. **To check if a model will work:** 1. Visit the model's Hugging Face page (e.g., [`amd/Qwen2.5-7B-Chat-awq-g128-int4-asym-fp16-onnx-hybrid`](https://huggingface.co/amd/Qwen2.5-7B-Chat-awq-g128-int4-asym-fp16-onnx-hybrid)). 2. Check the "Files and versions" tab to see the actual download size. 3. Add ~2-4 GB overhead for KV cache, activations, and runtime memory. 4. Ensure your system has sufficient RAM/VRAM. ### 4. **I'm looking for a model, but it's not listed in the Model Manager.** If a model isn't listed, it may not be compatible with your PC due to device or RAM limitations, or we just haven't added it to the `server_models.json` file yet. You can: - Add a custom model manually via the app's "Add a Model" interface or the [CLI pull command](https://lemonade-server.ai/docs/lemonade-cli/#options-for-pull). For advanced manual configuration, see the [Custom Model Configuration Guide](https://lemonade-server.ai/docs/server/custom-models/). - Use a pull request to add the model to the built-in `server_models.json` file. - Request support by opening a [GitHub issue](https://github.com/lemonade-sdk/lemonade/issues). If you are sure that a model should be listed, but you aren't seeing it, you can `lemonade config set disable_model_filtering=true` in to show all models supported by Lemonade on any PC configuration. But please note, this can show models that definitely won't work on your system. Alternatively if you are attempting to use GTT on your dGPU then you can `lemonade config set enable_dgpu_gtt=true` to filter using the combined memory pool. Please note ROCM does not support splitting memory across multiple pools, vulkan is likely required for this usecase. ### 5. **Is there a script or tool to convert models to Ryzen AI NPU format?** Yes, there's a guide on preparing your models for Ryzen AI NPU: ๐Ÿ‘‰ [Model Preparation Guide](https://ryzenai.docs.amd.com/en/latest/oga_model_prepare.html) ### 6. **What's the difference between GGUF and ONNX models?** - **GGUF**: Used with llama.cpp backend, supports CPU, and GPU via Vulkan or ROCm. - **ONNX**: Used with OnnxRuntime GenAI, supports NPU and NPU+iGPU Hybrid execution. ## Inference Behavior & Performance ### 1. **Can Lemonade print out stats like tokens per second?** Yes! Lemonade Server exposes a `/stats` endpoint that returns performance metrics from the most recent completion request: ```bash curl http://localhost:13305/api/v1/stats ``` Or, use `lemonade config set log_level=debug` to enable debug logging, or `lemonade logs` to view logs. ### 2. **How does Lemonade's performance compare to llama.cpp?** Lemonade supports llama.cpp as a backend, so performance is similar when using the same model and quantization. ### 3. **How can ROCm performance be improved for my use case?** File a detailed issue on TheRock repo for support: https://github.com/ROCm/TheRock ### 4. **How should dedicated GPU RAM be allocated on Strix Halo** **Linux** Please see the official AMD guidance [here](https://rocm.docs.amd.com/en/latest/how-to/system-optimization/strixhalo.html). **Windows** Strix Halo PCs can have up to 128 GB of unified RAM and Windows allows the user to allocate a portion of this to dedicated GPU RAM. We suggest setting dedicated GPU RAM to `64/64 (auto)`. > Note: On Windows, the GPU can access both unified RAM and dedicated GPU RAM, but the CPU is blocked from accessing dedicated GPU RAM. For this reason, allocating too much dedicated GPU RAM can interfere with model loading, which requires the CPU to access a substantial amount unified RAM. ## Hybrid and NPU Questions ### 1. **Does LLM inference with the NPU only work on Windows?** Today, NPU-only inference on Linux is available via FastFlowLM, see the guide [here](https://lemonade-server.ai/flm_npu_linux.html). At the moment, Ryzen AI SW's implementation of NPU and hybrid inference is currently supported only on Windows. ### 2. **I loaded a hybrid model, but the NPU is barely active. Is that expected?** Yes. In hybrid mode: - The NPU handles prompt processing. - The GPU handles token generation. - If your prompt is short, the NPU finishes quickly. Try a longer prompt to see more NPU activity. ### 3. **Does Lemonade work on older AMD processors or non-Ryzen AI systems?** Yes! Lemonade supports multiple execution modes: - **AMD Ryzen 7000/8000/200 series**: GPU acceleration via llama.cpp + Vulkan backend - **Systems with Radeon GPUs**: Yes - **Any x86 CPU**: Yes - **Intel/NVIDIA systems**: CPU inference, with GPU support via the llama.cpp + Vulkan backend While you won't get NPU acceleration on non-Ryzen AI 300 systems, you can still benefit from GPU acceleration and the OpenAI-compatible API. ### 4. **Is the NPU on the AMD Ryzen AI 7000/8000/200 series going to be supported for LLM inference?** No inference engine providers have plans to support NPUs prior to Ryzen AI 300-series, but you can still request this by filing an issue on their respective GitHubs: - Ryzen AI SW: https://github.com/amd/ryzenai-sw - FastFlowLM: https://github.com/FastFlowLM/FastFlowLM ### 5. **How do I know what model architectures are supported by the NPU?** AMD publishes pre-quantized and optimized models in their Hugging Face collections: - [Ryzen AI NPU Models](https://huggingface.co/collections/amd/ryzenai-15-llm-npu-models-6859846d7c13f81298990db0) - [Ryzen AI Hybrid Models](https://huggingface.co/collections/amd/ryzenai-15-llm-hybrid-models-6859a64b421b5c27e1e53899) To find the architecture of a specific model, click on any model in these collections and look for the "Base model" field, which will show you the underlying architecture (e.g., Llama, Qwen, Phi). ### 6. **How can I get better performance from the NPU?** Make sure that you've put the NPU in "Turbo" mode to get the best results. This is done by opening a terminal window and running the following commands: ```cmd cd C:\Windows\System32\AMD .\xrt-smi configure --pmode turbo ``` ## Remote Access ### 1. **How do I run Lemonade Server on one PC and access it from another?** Lemonade supports running the server on one machine while using the app from another machine on the same network. For detailed instructions and security considerations, see [Remote Server Connection](./server/configuration.md#remote-server-connection). ## Customization ### 1. **How do I use my own llama.cpp or whisper.cpp binaries?** Lemonade Server allows you to use custom `llama-server` or `whisper-server` binaries instead of the bundled ones by setting environment variables to the full path of your binary. ๐Ÿ‘‰ [Custom Backend Binaries](./server/configuration.md#custom-backend-binaries) ## TTS ### 1. **What voices are supported?** Lemonade supports most of the voices listed in [Kokoro-82M VOICES](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/VOICES.md). OpenAI voices can be selected from a default list, while other voices must be entered manually via text. ### 2. **Can voices be mixed?** Yes, two voices can be mixed using the following format: `af_jessica.5+af_kore.4` ### 3. **I entered a voice, but no audio is playing?** Some voices are not supported yet. If you manually enter a voice name instead of selecting one from the default list, and that voice is not supported, a muted audio will be generated. ## TTS ### 1. **What voices are supported?** Lemonade supports most of the voices listed in [Kokoro-82M VOICES](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/VOICES.md). OpenAI voices can be selected from a default list, while other voices must be entered manually via text. ### 2. **Can voices be mixed?** Yes, two voices can be mixed using the following format: `af_jessica.5+af_kore.4` ### 3. **I entered a voice, but no audio is playing?** Some voices are not supported yet. If you manually enter a voice name instead of selecting one from the default list, and that voice is not supported, a muted audio will be generated. ## Support & Roadmap ### 1. **What if I encounter installation or runtime errors?** Check the Lemonade Server logs via the App or tray icon. Common issues include model compatibility or outdated versions. ๐Ÿ‘‰ [Open an Issue on GitHub](https://github.com/lemonade-sdk/lemonade/issues) ### 2. **Lemonade is missing a feature I really want. What should I do?** Open a feature request on GitHub. We're actively shaping the roadmap based on user feedback. ### 3. **Do you plan to share a roadmap?** Yes! Check out the project README: ๐Ÿ‘‰ [Lemonade Roadmap](https://github.com/lemonade-sdk/lemonade#project-roadmap) lemonade-sdk-lemonade-dbde812/docs/favicon.ico000066400000000000000000003674311516551144000214230ustar00rootroot00000000000000 hf  จฮ00 จ%v@@ (B;€€ (F} ซin…(  Ÿงฌถ€”๕~…่ m—๘ะ>ดมnฎ˜คŽž8Š“P“๚ph˜๙ฤU้ @†าDˆึ‚ถˆด€นF|ณพuจ๑p›k๙_‰๐K‡แ๕B‚ฮB@„าะ1tjบqษlรJpฦๆoยjถdจ^›W๓SƒเUwฬฃYlณ WqปYศXฦaฮฮjำkหeฟ`ณYซMฃB›๐?๙@ƒว`@‡ิOล`ใQฮz_ืfุ`ฮZรXผQธDต7ฑ0ฉ๐1šุต:‰ธ FหCษPีฮ`^ูTฯPวQฤMฤAร4ย-พ-ฐ๊ึ5ŸะBฯAฮ@Pฺ๔^เTูIัJอPฮQะHั:ะ0ฬ0พ๔ั9ฌฺ=า=ฯ๚ZMWแIฺAิJิWุ[RBฺ5ิ4ฦ๗ซHฃอ6ฮ๖6ฬ๔NBฺ๚Kโ>๋ะG์ฅM่KNูM–fพ‘%ศ_4ิซบLโปูV๊ลฌK๊ม3<ท๏?ำ๖>ื๚?อ๙A๛">็$ต†=ไ)ฮข 9ฒPไพ @ใปtบว๘๘๐๐เเเเเภ€€ใ( @ ๒ฃ๒Šํ•~ใd›๛์Pฟฅิœ๛Šž&yžŒn–๘สgŠ๋ฎg?!psฦฟ›}กข˜ŸA“›r•๛—ŒŽ๘ก{–๛i›_š๚R”๐H‹฿ีD„อ!E…ะึ‘ฒ•ฏŽฎj‡ซศ€ฆ๔zกv›t”t๚o‰๕dŠ๓Z๐K๋9฿4‰ฯU4‹ำ‘ฎป…ท+ธดzธ๛sตnฏkฉhขf›d”cŒ๗a…๏\่V€แIุAษ€6‹แXqฐฑwพwผ-vฟหrยoยnพlทiฐfจcก`›]•๛ZŽ๔X‡์X€โZxื^rศัgkป&enฟtaจjภkฟkฤผkษmหoษnฤlฝhตeฎbจ_ข[V˜๙R“๒NŒ๊M„เRzาXrยฏZlฒYnทaภ`ธaลŠcฬiัmัnฮlศiมfบcณaฎ]ฉXฅRกM๘G˜๑C“้B‹E€อJxฝuEŽKtฐ]พXวXล?Zฬ์bิiึlิjะgษcรaฝ`ธ^ดZฐUญNชGงAฃ๗<Ÿ๑9š่8‘<†ศูA}ธ!@บSฤSภRสชYำcูiูhึeัaส^ล\ฟ[ปZธVถQดJฒBฐ;ญ6ซ๘3ง๑0 ็2•ึ8Šรf5Žห?„ตqตๆMษMศAPะ๐[ูdfฺcึ_ัZหWฦWยVฟUฝRผLบEน>ธ7ท2ต/ฒ๙-ญ๑,คใ2—ฯฅNx™<ฟNล๛Sฏ๕Iฬ™Qึ_dc^ึYัTฬRศRลRรQยOมJมCภ<ฟ6ฟ1พ.ผ,ธ๚+ฐํ0ฃูษ;”ม:˜วGศFว GะTฺ`฿b_YึSัOอMษNวOวPฦNวKวEว>ฦ7ฦ2ล/ฤ-ม,บ๕0ฌแี;žห; ฯBฬBสRFิ๚W`เ`฿ZSืMาJฮJหLสNหPหPอNอIฮBอ;อ5ฬ1ห.ศ-ม๚2ด็ฯ?คะ=งิHร๐:ึ?ฬ‚GืX฿^แ\฿UMืGำEะGฮKฮOฯSัTำSิNิGิ@ำ9า3ั/อ.ฦ๛4ธ๊ธIคหBฌืFรํgp›=อ๛กGูWเ\โXเPGุBิBาFัLาRิVึYุXฺSฺMฺDู=ุ6ี1า0ศ๛8ป๋IฏุCย๊Iฌำ:อ๙ชDูTแXใSเJBู>ึ@ีFีOืWฺ\^]฿X฿P฿H?8ู3ิ3ษ๙๙=ผ๊Q:ฟ๎Bมๆฎ7ห๕ž>ุMแRใMแE=:ู?ุHฺS]เcใeๅbๅ\ไSใJโA฿:4ี8ศ๗ิDปๆCฝ่Aฝเ.ะ๚5ว๎{7ิ๛C฿IใFโ>เ87>J฿Xโcๆj๊l๋h๋`๊V่LๅCโ;7ิ=ว๔ˆKผไ4ร็5มๅC1ฮ๕๔8>แ=โ8โ4แ5เ=แKไ[่h์p๐q๒m๑e๏Z์O้Dๅ;;ั๛ๆDล๑1Bว๓8ผ9ธู0ว๋ภ/ึ๛3฿4โ2ใ0ไ2ไ:ๆI้Zํh๒q๕t๗p๖h๔]๑Q์Eๆ>Aฯ๙‘`ชำLร์>ณา+ษ์2ภแ^+ะ๓๙+-แ-ไ-ๅ/็6้C์S๐b๕m๙q๛p๚h๘^๔R๎FๆCุูHห๖(Gฮ๗ˆ‚คamnŸeฉ•_ฌ‘ ?ฑี -ษ๊ฆ'ุ๙*เ,ใ-ๆ.้1๋;๎H๒W๖d๚kkf๛]๖Q๎Hแ๔Iิ๛_:๎Rศ๐]vhKฎ‘Lจ‹Nญ’pQฑ—ธOทœฮ=ฝŸฟ%มŸ รนR&ัํย(/โ0ๆ0้1์5๏>๓J๖W๚_c`๛W๕N่๘L€Sว๔Pะ๗Gฃƒ>ฏŒ?ญŠ!=ฒด;ท–<ผœ@มก>ลฅ0ษงฬงๅฮฒนึ฿แ/฿๚8ไ7่5์5๏9๒A๕J๘R๚V๚T๕O๋๎NเyQา๙ Pื๛2ฐ†3ฎƒ 0ตŒช+บ“,ภš1ล 9ษฆ?ฮซ?ัฎ5ิฐ'ืฐูด๙ อ5฿ํ๗?๗<ไ9้8ํ;๏@๑E๐Iํ๓K็ถNเGSั๚Qุ๛*ฐ…,ฎ‚ "ท‹~!ฟ”%ล›,หข8ะฉBีฐGูตEท?น9โผ4ๆพ๛/โฤ„Bะ๑LAึ๖=๚บ:฿ฬ;แษ>โฑCแ€G฿@Lุ๛ GไUบ๓'ธ‹*ฒ…$ฝ‘#$ร—x$ษžำ,ัง>ูฒPแปX็ยZ๋ฦXํศ๔Q๋ลŒEใฝ HโฟS™ปFรๅCอ๐Dฯ๓Kษ๐๏wjšุ;ธf“k,ร—)หŸj.ิฉบ=ดฺOๆฟาY๊ลคZ๊ฦLUไภW็รJฑ‰0ศ +ะฆ.ำช'สข2ูฒa0๐?๘๐เภ€€๘เภ?€€ภ๘(0` $Uด”๔ซ‰์ž†้ ฉzŽ๓S”๛Ÿœ๚ˆH{™๚ฃs’๔ฤqˆ๊ฆu~O…rฮ~vิ๑…๛jภฏ‘๛ช๙ฉ๗ฆ๗”—๛9‚œt ๑iŸa™๚[’๑Yˆๅ๔]€ืzarถ^yวธฯ™คกœŸG˜”šญ‘–ฦ“ั๛าŽ๘ี†๙๎t–gšbš]™๙S–๓F“๊B‹้Dƒห4D„อฆฉ๛คšซ!”ชzŽงอ‡ฅ๖ข|Ÿx›v—v’v๚vŠ๖q‰๔f‹๔^Ž๔X‘๒N’๏A“้66‰ฮm3Œึ’ฑ•ฐ Žฒb‰ฒึ€ฑyฏtฌpจnคl k›j–i‘iŒ๘i‡๔e„๏_‚์X„้Q‡็E‹ใ7Œ4‡ฬ“จ=€บ’ช‡ถˆต…ท”~น๗vบrนoถmฒkญiจgคfŸe›c–b‘๛aŒ๖`ˆ๑_ƒ์\ๆY|แV|KีC}ศพHuณ Iwน}บ~ธ|ผฆvฟqมpมoฟnผlทjฒhญfจeฃcŸa›_–]’๙\Ž๕Z‰๏Y„๊Yใ[z^uำ_sศ๒doฝOXvวpgฒsผtป sฟ›pรmวnวoวoฤnภmผjทhฑfฌdจbฃ`Ÿ^œ[˜Y”๘V๓TŒ๎R‡่S‚แU|ูZuฯboยีfjท1cmพzY™kฝlฒkภwiล๚iสkฬnอoหoศnฤlฟjบhตfฐdฌbจ_ค]กZVš๛S—๖P“๒MํL‹็K†เLืRwหYqพฝ[lฒZmตrฒcรdมBcฦ้cฬgะlัnะoฮnสlฦjมhฝfธdดcฐaฌ^ฉ\ฆXฃT P๚Lš๖I—๑F”์DๆCŠ฿D„ีJ{วOtบ†WeŽRpญ^ย_ภ]ฦป^ฬbาhิlิmำmะkฬiศgรeฟcบbถaณ_ฐ]ญZซVจRฆMคIก๙EŸ๕Bœ๑?˜์=”ๅ<=‡าC~ย็Hxต5Gyธ]ฟUอXลjXห๛]าdึiืkึkิjัhอeษcฤaภ`ผ_น^ถ]ดZฒWฐSญOซJชEจAฆ๙=ฃ๕:ก๑8์6™ๅ6“9Šอ?ฝ’YaxD|ฒUฤUรSษัVั^ึeูiูjืiีfาdอaษ_ล^ม]พ\ป[นZทXตTดPฒLฑGฏBฎ=ฌ9ช๚6จ๖4ฅ๑2ข๋1ไ1•ุ8‹ว>‚ธ!=ƒบVม๛KัPวrPฮXี`ูehฺgุeีcา_ฮ]ส[ฦZรYภYพXผWบUนQธMทIถCต?ด:ณ6ฑ3ฐ๛1ญ๗/ช๒.ฆ๋- แ1•ั๛9‹มY6ฤE‡ณPลPฤMหอPำZูbefeูbี_า[ฮXสWวVฤVยUภUฟTพRฝOผKปFปAบ<น8น4ธ1ถ/ต-ฒ๗,ฏ๑+ฉ้.Ÿฺ5”ษณAŽผQย๙IสKศ]Jฯ๛Sื]cedbู^ึ[าWฮTหSศRฦSฤSยSมRมPภMภIภDฟ@ฟ;พ7พ3ฝ0ผ/ป-น,ถ๗+ฑ๏,จโ3œะถCŒต=“มMล๚Pภ๙GหฏJำUฺ^cdb_ู[ึWาSฯQฬPษOวPฦPลQฤPฤOฤLฤIฤEร@ร;ร7ร3ย1ม/ภ-ฟ,ผ+ธ๕,ฐ้2ฃืส?—ย=šวHศHว๛,Dฮ็LืX_฿b฿b_[ูWึSาOฯMฬMสMษNศOวPวPวOศMศJศFศBศ=ศ9ว5ว2ฦ0ล.ฤ-ม,ฝ๙,ถ๎3ฉฯ@ษ?ŸฬAหDษdCัNฺY_เaเ`฿\XูSึOาLฯJอJหKสLสNสOสPหPหOฬLฬIฬEฬ@ฬ;ฬ7ห4ห1ส/ษ-ฦ,ย-บ๑4ฎ฿สCกฬAคะJฤ๓ษ@hAสšCิPZเ_แ`แ]฿YTูOึKำHะGฮHอJฬLฬNอPอQฮRฯQะPัMัHัCั?ะ:ะ6ฯ3ฮ0อ.ส-ฦ.พ๓6ฒโธLกศDจำFฤ๓Gม๑ ?ฬยCึQZเ^โ^แZ฿UPูKึGำEัEฯFฯIฮLฯOะRัTาUำUิSีPีLีGีBิ=ิ9ำ5า1ะ/อ.ษ0ภ๔9ดใ˜KชำCล๒Bฤ๑=อ๛ูCืQYแ\โ[แW฿RLฺGืCิBาCัEัHัLาPำTิVึXืXุVูTูPูKูEุ@ื;ื7ี3ำ0ะ/ห2ภ๓=ถใk6บ้Lญึ@ล๐@ฤ๐$;อ๛ไBุOXโZโYแTเOIฺCุ@ี?ิAำDำIิNีSึWุZฺ[[ZWSMHB=ฺ8ุ4ึ1า0ฬ6ภ๑๏Bทโ8@ธไ?ล๎>ฤํ&9อ๙ๅ@ุM฿UโWใUโQเKE@ู=ื=ึ@ีEึKืQุVฺ[^_฿_เ]เZเUเP฿J฿D>:ฺ6ุ2ิ2ฬ:ภ๏ฦJตGธแ>ร๊>ย่7ห๖<ืIQโSใQใMแGA=ฺ;ุ<ุ@ุFูMฺT[`เcโdใcไ`ไ\ไWใQโKแEเ@฿;7ู3ิ5ส๚?ฟ์"าPถ>ภๅ?ฝแ5ษ๑ห7ีCKแNใMใIโCเ=98ฺ;ฺ@ฺGPXเ_ใdๅh็i่h่d่`็ZๆSๅMไFโAแ<7ฺ5ิ:ศ๗๋Eพ้5Cภ๋@ผ฿Uงม5ล๋ค3ั๚ใ:โ6แ4เ4เ8เ?แIโSๅ^่f๋l๎p๐q๑o๑k๐f๏_ํW๋P้I็Bไ=เ8ฺ;ฮ๛๕Eฤ๏NAว๓Žกท8ฝ฿9ผ*1ฦ๋ใ/ำ๚48฿9แ8โ6ใ3โ1โ2โ6ใ>ไHๆS่^๋g๎n๑r๓s๔r๕n๔h๒a๐Z๎R์J้Cๅ=เ:ืAห๗ถOฟ็ Kร๋=ทุKจล3ภใ›-อ๓.ุ12เ2โ1ใ0ไ/ไ0ไ4ๅ<็E้Q๋\๏f๒n๕r๗t๘s๘o๗j๕c๓[๑S๎K๊Dๅ>฿?ำ๓Gษ๓MBอ๘จทFฌส4ผ7บฺ;.ฦ้๊*ี๙,.฿.โ.ใ-ๅ-ๅ.ๆ2็8้A๋L๎X๑b๔j๗p๙r๚r๚o๙j๘c๖\๓T๏L๋Dๅ@Eฯ๙กTภ่Oฦ๏;ดำHฃฟ2ภเ*ฯ๑(ฺ*,แ,ใ,ๅ,ๆ-่/้4๋<ํF๏Q๒[๕d๘k๛oomi๚c๘\๔T๐L๊EโEึฺMฬ๔-Jฮ๗ “ฉฟTo~–Žsข“ sฆ• Šก–8บื8ธุ-ศๆว&ึ๖(*เ,ใ-ๅ-็-้/๊2์7๎?๐I๓S๖\๙c๛hkjga๙[๕R๏K็H๒Lา๙\8๋Uศ๎SšƒR‘zTฅ)Wช’vXฎ–ญUฒ™ฤLถœม;บข+ฝd!ผ–3ฟ8'ฯ์ื%ฺ๙)฿-โ/ไ/็/้0๋1ํ4๏:๑A๓J๖S๘[๛`ddb^๙W๔OํKแ๙Mึ๛~Xฦ๎Sฮ๔EฆˆCข„ GซmHฐ“เGด—Gธ›HผžDฟก8ยข'ลฃฦขฺฦก‚ ววh%ำํำ(๚/แ3ไ3ๆ3่2๋2ํ4๏7๑<๓C๕K๘R๚X\]\๚W๗Q๐Mๅ๗N†Tฮ๖ Rำ๙>ช†=งƒ ?ฏŒ†<ด‘๗6ธ–5ผš8ฟž<ยก@ลฅ@ศง9สฉ*อฉฮจฯง฿ฯตบีแ,๘9โ:ไ8็7้5์5๎6๐9๒>๔D๖J๘O๙S๚T๙S๕O๏Nๆ้OpUั๗Rึ๚7ฌ…:˜l9ฑŠr3ถ๘,บ”+พ˜.ม3ลก8ศค<สจ?อซ>ฯญ:ัฎ0ิฏ%ีฏืฎุณ๗ฺห.฿์>฿๛?เ=ๅ:่8๋7ํ8๏;๑>๒B๓F๔I๓J๏K้๗LใตO?Yอ๖Tี๙.ฏ„0ฎƒ)ต‰ฬ"บ$ฟ–)ร›-ฦŸ1ษฃ7ฬง=ฯซAาฎBีฑAืณ=ุด7ฺด/ถ(฿ท!แน"ใย1ูฦBำ๓ฝAุ๘๏>;โ9ๆ8่9้;๊>้A่DๅไHโชKRPื๚ MXษ๖#ฏ‚%ช}ด‡=ปŽ  ภ”้"ลš'ษŸ-ฬค5ะฉ>ิฎDืฒHฺตIธGนD฿บ@แป<ไพ;็ม9็ฟ/โท3Iผ่ Cห๊8Bะ๑n@ิ๕›=ุ๘ถ<ฺ๚ร<๛ภ>๛ฎA๛ŠD๛YIู๚'Oั๗Jู๚[บํ+น0ถ‹'ฝ‘4$ม•"ฦšโ#ห *ัง9ึฎFตO฿บTใพUๅมU่รV๋ฦW๎ษS์วKๆภI\ใKตQณฯXจมJมเGศ๊ Hส์ Rรๆ‹‚ถtšศ8บDณ‹.ภ•*(ลš„%ห )าง6ฺฐHแบW่ม`์วc๐หb๐ฬ๕]ํษฑU็ย5Jุณ@ป’Kฒ‹3ฤ™!-ห m,างด1ุฎี<ตูGโปฤMไพ“MโฝHFฺต OๆมJ=rฆ{<ฤ›/ฬข,ฯฆ+ฬค ฒ‹ผ•วภ๘เภ€?๘๘๐๐เเเเภภภภภภภเเเ?๐?๐๘๐เภภภ๐?เ(@€ @้[ฏทƒ่ฎ‚ๆฦwต|฿ศ•๒J —›๚ˆ™๙f~•๗ฉyŽ๑บy†้›~~เPŒuี ||Eฉไš๓šฃ™๚ T€ŸวsŸiœc–๘`๐b‡ๆ๖j~งqwฯ k{ึšcฎฯ‘แฑ– จ•ฅ“0ฃ‘๛=ข๙BขŽ๘AกŽ๗B˜“๙`‰˜ฏz๔n h d_™๚X–๔P‘ํO‰โUิฉYyย W|วดฟ›ฆกŸŸM›ž‹—œฝ“š—์Ž”๓‘๖๚๖๙๖ŠŽ๘}‘๚o•g˜cš`™๛\˜๘T—๔J•๎A’็?Šู๕CƒสIC„อคงจฆจ-˜ง„“ฆัŒค๘†ฃก}žz›x˜w•w‘x๚xŠ๗w‰๕p‰๔gŒ๕`๖\‘๕X“๓P”๐F”์<”ๆ57ˆอ„,“้>ƒฝฅฃ„ป˜ฎ“ฎ†ฎใ…ญ~ฌyชuจsฅqขoŸn›m˜m”llŒ๙m‰๖l†๒g„๐`„๎[†ํV‰์PŒ๋GŽ่<ไ44‰อซCyจ:„ภ’ฑ–ฏณJŠณฬƒต{ตvดsฒpฏnฌmจkฅjขižh›g—f”f๛eŒ๘eˆ๔d…๐a‚์]€้Y€ๆUไO„แD‡9ˆุ5„หส<~น;ฝŠด‹ณ‡ถn‚ธ๋zปuปrปpนoถmณlฐjฌiจgฅfกežd›c—b”a๚`๖`‰๒_…๎^‚๊\~ๅ[|แZ{W{ูM}ำD|ศ๋Exป2Eyฝ€ทตน|{ผ๕tฟqมpภoฟoฝnบlทkดiฐhฌfจeฅdกcžb›`—_”^‘๘\Ž๕[Š๑Z†ํZƒ่Zใ[{]wุ`tา^tษ_qพ‰|^กjjตyนzตxผtuฟ๖pรnลnลoลoรoมnพmปkทjณhฏfซeจdคbกaž_›]˜\•๛Z’๗X๔VŒ๐Uˆ์U…็UโW|Zwึ_sฮeoร๎ikบXOฃkfฐtบjหrฝZoม๏lฦkศmษnสoษoวoฤnมmพkบiถhฒfฎeซcจbค`ข_Ÿ\œZšX—๙V”๖T‘๓RŽ๏P‹๊P‡ๆPƒแPSyิYtห`nฟแckถ?bmผdgฎnบkภlพ4jยgวhหjอmอoอoฬoสoวnฤlภjผiนgตfฑdฎcซaจ`ฅ^ฃ\ YžV›T™๘Q—๕O”๒M‘๎LŽ๊JŠๅJ†เJ‚ฺM|าRvศYpผศ[lฑ [mดfฟgพeยดcศcฬgฯjะmัnะoฮoฬnษlลkยiพgปfทeดcฑbฎaซ_ฉ]ฆZคXขU Rž๛Oœ๘M™๕J—๑H”ํF‘้EŽไDŠ฿D…ุFะMxฤRrธ”VkคToฎdฟ6aยs_ว_อcะgาkำmำnาnะmอlสjวhรgภeผdนcถbณaฑ`ฎ^ฌ\ชYจVฆSคPขM ๚Jž๗Gœ๔Dš๑B˜ํA•้?‘ไ>?ˆืB‚อHzฟ๏LuดGKwธPoง]ย]ม.[ฦ[ฬ_ัdิhีkีlีmำlัkฮiหgวfฤdมcพbปaธ`ต_ณ^ฑ\ฏZญXฌTชQจNงJฅGฃ๚Dข๗A ๔?ž๑=›ํ;˜้:•ใ9:‹ี>„ษD|ปฏItซ GwฐZม]ผXล™WหZั`ิeึhืkืkึkิjาhฯfหdศcลaย`ฟ`ผ_บ^ธ^ถ\ด[ฒXฑVฏRฎOฌKซHชDจAง๚>ฅ๗<ฃ๕9ก๑8Ÿํ6œ่5˜ใ5“7า<„ร๑B}ถBA~ธzคโUลVฤ>Sษ์Uฯ[ิaืeุhูjุjืiิgาeฯcฬaษ`ฦ_ร^ภ]พ]ป\บ[ธZทXตVดSณPฒMฑIฏEฎBญ>ฌ;ช๛9ฉ๘6ง๕5ฅ๑3ขํ2Ÿ่1›โ1•ู6ฬ<„ฝ”a]uB~ฒUยXฝRวขQอUำ]ืbูfฺhฺiูhืfีdาbฯ`ฬ^ษ]ฦ[ฤ[ม[ฟZฝZผYบXนVธTทQถNตJดFณCณ?ฒ<ฑ9ฐ6ฎ๛4ญ๙2ซ๕1ฉ๒/ฆํ.ข็.เ0–ี7Œลึ?„ถ=…นPฦQลพ:พ7ฝ4ฝ2ผ0ป/ป-น,ท๚+ต๖+ฑ๐+ซ่.ฃ5™ห dr‹>’ภPร๘CฮJวtGฮMีVฺ]addcaฺ^ุ\ีYำVะSอRหQษPวQฦQลQฤQรQรPรOรNยKยHยEยAย>ม:ม7ม4ภ2ภ0ฟ/ฟ.พ-ผ,บ๙+ถ๔+ฑํ-ฉแ4žะบDน>–รMล๚Oร๙GสภFาOุX^b฿c฿ca_ฺ\ุYีUำSะQอOหOสNศOวOฦPฦPลPลPลOลMลKลIลFลBล?ล;ล8ฤ5ฤ2ฤ1ร/ย.ย-ภ,พ+ป๘+ถ๑-ฎๅ4ฃีฦC˜ย@›วHวIฦ๛9Dอ๏GิQฺY_฿bเb฿a_]ฺYุVีRำPะNฮMฬLหLษMศNศNศOวPศPศOศNศLศJษGษDศ@ศ=ศ9ศ6ศ4ว2ว0ฦ/ล-ฤ-ย,ฟ๛,บ๔.ฒ่5งุษDœวBŸหNม๐>ฮEศ๛uBฯIืSZ฿_เaเaเ`]ZฺVุSีPำMะLฮKอJหKสLสMสNษOสPสPสPหOหNฬLฬIฬFฬCฬ?ฬ;ห8ห6ห3ส1ส/ษ.ว-ล,ย,พ๗.ถ๋6ซรGŸษ CขอJล๔PพํBสญAาJูT[฿_แ`แ`เ^[WฺSุPีMำKัIฯHอIฬJฬKหLหNหOฬPฬQอQฮQฮPฯNฯLฯIฯEฯBฯ>ฯ:ฮ7ฮ5อ2อ0ฬ/ส.ศ-ล-ภ๙/นํ8ฎตNŸลDงัGล๕Gฤ๔@หีAิLฺU[เ^แ_แ^เ\฿XTฺPุMึJำHัGะFฮGฮHอJอLอNอPฮQฯRะSัSาRาQำNำKำHำDาAา=า9ั6ั4ะ1ฯ0อ.ห.ศ-ร๚0บํ;ฐš&HฉาCฦ๕Dล๕2>ฬํBีLU฿[แ^โ^โ]เZ฿VRNุJึGิEาDัEฯFฯGฯJฯLฯOะQัSาTำUิUีUีSึQึNึKึGีCี?ี<ิ8ำ5ำ3ั1ะ/อ.ส.ฤ๛3ปํ>ฒu5ทๆPซำ@ว๖Aฦ๔K<อ๙BึMU฿Zแ]โ]โ[แW฿SOKูGึDิCำBาCัEะGะJัMัOาRำTิVีWืXืWุVูTูQูMูIุEุAื>ื:ึ7ี4ิ2า0ฯ/ห/ล๚6ป๋๗AณJ?ดเ<ศ๗?ฦ๓_;ฮAืLTเYโ[โ[โXแU฿QLHูDืBี@ิ@ำBาDาGาKำNิQีTึWืYุZูZฺZXVSPLHฺCฺ?ู;ุ8ื5ึ3ิ1ั0อ0ฦ๙9ป๊Gณ!Eด:ษ๖>ฦ๑i9ฮ@ืJRเWโYใYโVแR฿NIEฺAุ?ึ>ี?ิAิDิHิLีPึSืWูYฺ[]]\[XURNIEA=ฺ9ู6ุ4ี1า0อ3ล๗=ป่ฏUญาKณ9ศ๔=ล๏h8ฮ๚>ืHPเUโWใVใSแPเKGBฺ?ุ=ื=ึ>ึ@ึDึIืMุRูVฺZ]_`฿`เ_แ]แZแWแSเOเK฿FB>:7ู4ื2ำ1อ6ฤ๔Bปๆl8ภ๏Qดู9ฦ๏<ฤ์\6อ๘;ึEMเRโTใSใQโMเHD@=ู;ุ;ุ=ื@ุEุJูOฺTY]฿`เbโcใcใbไ`ไ]ใYใTโPโLแGเC฿?;85ุ3ิ3อ;ย๐ๅHบใ,Fปๅ;ร้<ย่F5ห๔๗8ีAI฿NโPใPใMโJแE฿A=:ฺ9ฺ:ู<ู@ูEฺLQW฿]แaโdไfๅgๆg็e็b็_ๆ[ๅVๅQไMใHโDแ@เ<86ุ4ิ6ห๛@มํฉXฒืMนโ<ภๅ=ฟไ+4ศ๐่4ำ<ฺDIแLใLใJใFโBเ>฿:879ฝเ@ป5ล๊ศ1ฯ๘7ุ>CเFโGใEใBโ?แ;เ8฿667;AG฿OแVใ]ๅb็g้k๋m์nํmํkํh์d์_๋Z๊T่O็JๆEไAโ=เ97ุ7ะ@ฦ๒ศNผไKพ็Aบฺ6มๅ‘0ห๒2ี8=฿@แAโ@ใ>โ;โ8แ6เ4เ4฿6฿:เ@เHแOใWๅ_่e๊j์n๎p๏q๐p๐n๐j๐f๏aํ\์V๋Q้K็FๆAใ=เ97ื;อ๚Eฤ๎o8อOผไ6ฟแ8ฝ฿M1ฦ๋๗/ั๙3ุ7:เ<แ;โ:ใ8โ5โ3โ2แ2แ5แ9โ@ใGไOๆX่_๊fํl๏p๑r๒s๓r๓p๓m๒h๑c๐^๎XํR๋M้G็Bไ>แ:9ิAส๖าMม้Jร์<นฺ=ทื3มไษ-อ๓/ึ256เ6โ6โ4ใ2ใ1ใ0ใ1ใ4ใ8ไ>ๅF็N้W๋_ํf๏l๒p๓s๕t๖s๖q๖n๕j๔d๒_๑Y๏SํN๋H่Cๅ>เ;>ัGว๑t*Uพไ>ถึ)ฦ๊6ผr.ว๊,า๘.ู01฿2แ2โ1ใ0ไ/ไ/ไ0ๅ2ๅ6ๆ<็D้L๋Uํ]๏d๒k๔p๖s๗t๘t๘r๘o๗k๖f๔`๓Z๑T๎N์I้Cๅ?เ=ุDอ๘ฬOฤ์Lฦ๏9ธุ;ถี2ภโา+ฮ๑*ื,..เ/โ.ใ.ไ-ๅ-ๅ.ๆ0็4่:้A๊I์Q๎Z๑a๓h๖m๘q๙s๚s๚r๚o๙k๘f๖a๔[๒U๐OํI้Cไ?Bำ๗Jส๔^AาRร๋Cฎอ ะ๔6บฺm-ว้๛)ิ๗)ฺ+,เ-โ-ใ-ไ,ๅ-ๆ-็/่2้7๊=์DํM๐U๒]๕d๗j๙n๚p๛qpn๛k๙f๘a๖[๔U๑OํI่CใBูHฯ๘จTล๋ Pศ๏9ทึ<ณา2ภเท)ฯ๐'ู๛(*฿+แ,ใ,ไ,ๆ,็-่.้0๊4๋9ํ@๎G๑O๓W๕^๘d๚i๛lnnli๛e๙`๗[๔U๑N์H็D฿HิูOห๓.Lฮ๗ฎษ?y8ิŸž‰‹ƒ›“‚Ÿ”››”p“š”ฏ2ฟ7นื7-วๅ%ี๔&(*เ,โ-ไ-ๆ-็-่.๊/๋2์6๎;๏B๑I๓Q๕X๘^๚cgijigc๚_๘Y๕S๐M๋HใIู๐Nะ๘Y9ๅXศ๎[žŠ\™†[ฃ1[ง’p\ซ•ŸZฎ—ธUฒ™ปKต›ช?ธœ‚7บœG5ธš3ฟT(อ้๋$ุ๗'*฿-โ.ไ/ๅ/็/้/๊0๋1ํ4๎8๐=๒C๔J๖Q๘W๚]๛adeec`๛\๘W๔Q๏K็J๘Nิ๚{\ฤ้Uฬ๑H˜€gํภKค‰*Nช›Oฎ“้Nฒ—NตšMธœHปž?พ 1ภ $ม ๕ยŸฦยŸnพ.ลเ^%ั์่%ฺ๗).แ1ใ1ๅ1็1่1๊1์2ํ3๏6๐9๒?๓D๕J๗P๙V๛[^``_\๚X๖S๑N๊Lแ๙Oื๛ŠVฬ๔ Sั๗?ก)€]Dฉ‰JEฏึCด”?ท—?นš@ผCพŸDภขCยค=ลฅ1วฅ!ษฅษคศฃิวขz!ษฮu$ิ์'๘.เ5โ6ไ5ๆ4่4๊4๋3ํ4๎5๐8๑;๓@๕E๖J๘P๙T๛WZZ๛Y๚V๗R๒N์Mโ๔OฺƒUะ๕ Sิ๗9ข”?ฌ‰Q?ฑๅ9ถ“3ธ•2ป˜4พœ7ภŸ:ยก>ลค@วฆ?ษจ<หฉ4อช'ฮชะชะจฯงฺฯทนี฿แ)๕6แ;ใ:ไ9ๆ8่7๊6์6ํ6๏7๐9๒=๓A๕E๖I๘N๙Q๙R๙S๘R๕O๑M๊MโโPฺeWฯ๕Tิ๘6ง‚=ฒ‹;ฎˆ<9ณ฿1ท‘,บ”+ฝ—-ภ›0ยž3ฤก7วค;ษฆ>หฉ?อช>ฮฌ;ะญ6าฎ-ำฎ$ีฎึฎืญืฒ๔ุส)๊;เ?เ>แ=ๅ;็9๊8์7ํ8๏8๐:๑=๒@๔D๔G๕I๕K๔K๑K์Kๆ๗MเฏQฺ๛6eม๋Wั๕2ญƒ4ฌ‚3ณ‰ป*ทŽ%ป’'พ–)มš,ร/ฦ 2ศฃ6สฅ:อจ>ฯซ@ัญ@าฏ@ิฐ=ีฑ8ึฒ3ุฒ,ูณ%ดต฿ถเภ&แื:ฺํ๓Aึ๗๚@?แ=ไ:็9้8๋8์9ํ;๎=๏?๏A๎C์E้Gๅ๓JแนNWSี๘ Neพ๋*ฎ‚,ญ#ณ‡ฎน๙พ“#ม—'ฤ›*วž-ษก1หค6อง;ะซ?าฎBิฐDึฒDุดCูต@ฺถ<ถ8ท4ธ/เบ+ใผ)ๅฝ'ๅฝ๕-ฦkDส๊\Bฯ๏ฌAิ๔เ?ุ๙๙=;เ9โ8ไ8ๅ:ๅ<ๅ>ไAโ๏DเหGŽKฺ๛CQึ๘ =์|กหซ&ธ‹ ฒ…!ธ‹W ผถภ”๓ ฤ™#ศ(หก.อค4ะจ;ำฌAึฐEุณHฺตJธJนHบF฿ปCแผ@โฝ>ๅฟ>็ม?้ย;ๅฝ˜2ณ 8ยHพูEศๆ#Cฬ์NAะ๐y@ำ๔š>ี๖ฏ=ื๘ธ>ู๙ต?ู๙งBฺ๙ŠDู๙bGุ๙6Lิ๖]ล้Uฬ๐Bฏ†%ฝ‘1ท )ฝ‘H%ภ”จ!ฤ˜ํ ษ#อข+ัง6ิฌAุฑHตNนQแผSโพSไฟRๅมQ็รR๊ลTํศTํศN่รฆEเบGโฝXณฮ`ฃฝPบืKรโNรใ[นุˆญzœบญ†l)ฟ”=ท/ฟ”<)ร˜›$ศœ่"อก'าจ4ุฎCตOแปWๅภ]้ฤa์วc๏สf๑อf๓ฯ`๐ฬ๒V้ลL฿ปOไฟ9ผ“@ธ2ม—3,ฦ›(ฬก฿'าง.ุฎ<ตJไผV้ย]์ว^๎ศ\ํศ๑W้ฤถQใฟMDุณJน?พ”Kต7ฤš&1ห l-ะฅฌ-ีชฮ2ูฏุ:ดะ@ทณDธ€Dถ>=ิฏ Lๅฟ%น˜u[Cภ˜4ษ /ฬฃ.ฬค+ฦ~Vชƒ๐ภ€๐เ€๘๐?๐?เภภ€€??€€แภ ๘๘?๐เเ๐ƒ?ภ๐(€ ^ฦ^ฦ^ฦ^ฦช“๔ณ“๒”๔!•’๓U๒}ŒŒ๏Œ‡๊†Žƒ็h’โ7œyeำฐsฺฉš๗ฝš๓›š๚.™๙І™๙ุ}—๘๖w•๗s‘๔r๐t‰์y„ๆ๊€~แถˆz]“uีP€โทpะญš๕qpŸ›๚“œ๛y‡{žržlœhš๛e—๙c“๖b๒b‹ํf†็o€เ๖yzูณ‚vำ:กlย‰tฯฆฬ™™ฅ™๘ ˜›๛Rย€Ÿ๚uกnกj gŸdœb™๛_—๘]”๕Z‘๑X์Yˆๆ`‚฿l{ึtwฯPŠfญxtศถ”๕ม“๔ง”๘—๚I’™๛ง‡œ๑{Ÿrกmกjกh fŸcaš^˜๙[–๖W”๓S‘๏QŽ๊PŠๅU„`}ำgxษ>a|ัspปธ๗ฟ‹๑ฒ“ญ•ช“$ฉ“๛<จ’๛Qง’๚aฆ๚jฆ๙pฅŽ๘rฅ๘qฅ๗mคŽ๗lขŽ๗sž‘๘Ž—”๙ย—๛๐šwœpžlŸiŸhŸfŸdbœ`š๛]™๙Y—๖U–๓P”๐L’์I็H‹โL…ูV~อท\yมZ{ลส’ฐšฉœง›Kคš{ก™ฆŸ˜ส—ไ›–๎™•๕˜”๚—’–‘–๛–๚–Ž๙–Ž๘•Ž๘‘๘Š‘๙“๛x–q˜lšh›fœeœdœb›`š๛^™๚[˜๘W—๕S–๓N•๐H”์D’้Aไ@ŒE„า๖LฦWIสWyปฉ ซ ฆก'ค d Ÿคžู™ž๓•‘œŽœ‹š‰™‡—†–…•…“…‘……Ž๛…๚…Œ๘†Œ๗„Œ๗€๗y๘r‘๚l“๛g•d–c—a˜`˜๛_˜๙\—๘Y—๖U—๔P–๒K–๏F•์A”้=“ๅ:เ;Šึ@ƒษกH}ทD€ภชกฒงคขคPŸฃŸ›ฃ–ฃ๚‘ขŒขˆก… ƒŸž~›}š|˜{–{•{“{‘{{Ž๛{Œ๙|‹๘|Š๖|‰๕y‰๕tŠ๕nŒ๖iŽ๗d๘a‘๘`“๘^”๘\”๗Z”๕W•๔S•๒O•๐I•๎D•๋?”้:”ๅ7’แ5Žู9‡ฬึ@‚ฟ>ƒรฅฅญกกงžจg›งภ–ง๔‘ง‹ง†งƒฆ€ฅ~ค|ฃzกy wžvu›ušt˜t–t”t“t‘st๚t‹๘uŠ๗uˆ๕t‡๔r‡๓n‡๓jˆ๒eŠ๓a‹๔_๔]Ž๔[๔X‘๓U‘๑Q’๐N“๎H“์C”๊>”่9”ไ5“เ2ฺ5‰ฮํ;„ม;9…ฤฌญƒงžซ›ซc˜ชว“ซ๙ฌ‡ซƒซช}ฉ{จyงwฆvฅtฃsขr qŸqp›pšo˜o–o”o’nn๛o๙o‹๘o‰๖o‡๔o†๓m…๑j…๐e…๐a…๏^‡๏\ˆ๏ZŠ๏W‹๏TŒ๎QํM์H๊C‘่>’ๆ9’ใ5’เ2ฺ3Šฯ๙8„ย^7†ลœซŸฉ™ญB–ฎต‘ฎ๗‹ฏ…ฐ€ฐ}ฏ{ฎxญwฌuซsฉrจqฆpฅpฃoขn mŸml›lšk˜k–k”j’jjŽ๛jŒ๙kŠ๗k‰๕k‡๓j…๒h„๐fƒ๏bƒํ_ƒ์\ƒ๋Z„๊W…๊U‡๊R‰้OŠ่J‹็Eๆ?Žไ:แ52Žู2‰ฯ7„ร|2ƒวB‹ฟคฑŠฎ—ฐ”ฐฑ่Šฒ„ณด|ณyณwฒuฑsฐrฎqญpฌoชnจmงmฅlฃkขj jŸii›i™h—h–g”g’ggŽ๚gŒ๘gŠ๖fˆ๕f†๓f…๑eƒ๏c‚ํa๋^€๊[€่Y€็WๆUƒๅR„ๅN†ไIˆใC‰แ=Š฿8‹4‹ุ2‡ฮ7ƒร›K}ฌ?บฏŠŸ’ฑ4ฒถŠด๛„ถท{ทxทvถtถsตrณpฒpฑoฏnญmฌlชkฉkงjฅiฃiขh hžgœg›f™e—e–e”d’d๛dŽ๙dŒ๗dŠ๖cˆ๓c‡๑c…๏bƒํ`์_€๊]่[~ๆY~ไX~ใVแT€เP‚฿KƒD…>†ู8†ี5„ฮ8€ยภ?|ถ=~บฏ•ชณRŠตื…ทน{บxบuบtบrนqธpทpถoดnณmฑlฐlฎkฌjชiฉiงhฅgฃgขf fžeœe›d™d—c•c”b’b๚aŽ๙aŒ๗a‹๕a‰๓`‡๑`…๏`ƒ์_๊^€่]~ๆ\}ไZ|โY{เY{W|U}P~ูI€ึBำ<€อ<}ยไAyธ)?zบŒดŽณ‰ดi†ท้น{ปwผuฝsฝrฝqผpปpบoนnธnทmตlณlฒkฐjฎiฌiชhจgงgฅfฃfกe eždœcšc™b—b•a”a’๛`๚`๘_๖_‹๔^‰๒^‡๐^…๎]ƒ์]‚้\€็\~ๅ\|โ[{เ[y[y\xู[xึXyิRzะK{ฬFzร๚HwนcG{ภIlช„ด…ณ „ถw‚น๐}ปxพuฟsฟqภqฟpฟpพoพoฝnผnบnนmทlตkดkฒjฐiฎhฌhชgจfงfฅeฃdกdŸcžcœbšb™a—`–`”_’๛^‘๙^๗]๕]Œ๓\Š๑\ˆ๏\†ํ[„๋[‚้[€ๆ[~ไ[|แ\z฿\x^wู_uึ`uำ`uฯ]uหXuลUsปถVpฏVqดถ‚ดธz~บ๓yฝuฟsภqมpยpยoมoมoมoภoฟnพnผmปmนlทkตjดjฒiฐhฎgฌgชfจeฆeคdฃdกcŸbžbœa›`™`˜_–^”๛]“๚]‘๘\๖[Ž๔[Œ๓ZŠ๑Y‰๏Y‡์Y…๊Yƒ่YๅYใZ}เ[{\x^vื`tิbsะdrอerศdpภ๖emธgŠG…iiฒ~ถ€ณ}นr{ผ๒vฟsมpยoรoฤoฤoฤoฤoรoรoยoมoฟnพmฝmปlนlทkตjณiฑhฏgฎgฌfชfจeฆdคcฃcกbŸaž`œ`›_™^˜]—\•๛\“๙[’๗Z‘๖Y๔X๒W‹๐WŠ๎Vˆ์V†๊V„็V‚ๅV€โW~เX|Yyฺ[wึ]tา`rฮdqสgoลjmฝ฿liถBemหoiญyธzณyนbwฝํsภpยnฤnลnฦnฦnวnวoฦoลoลoฤoรoมnภnพmฝmปkนkทjตiณiฑgฏgญfซeฉeจdฆcคbฃbกa `ž__›^š]™\—[–๚Z”๙Y“๗X‘๕W๓VŽ๒U๐U‹๎T‰๋T‡้S…็SƒไSแT฿U}VzูXwีZuั]rอapศfnยikปอkhณ/ilปm`งuทvษvบJtฝใqมnรlลlฦlศlศmษnศnษoศoวoวoฦoฤoรoมnภmพmผlบkนjทiตhณhฑgฏfญeซeฉdจcฆbคbฃaก` _ž^]œ\š[™Z˜๛Y–๚X•๘W”๖V’๕U‘๓T๑SŽ๏RŒํQŠ๋Qˆ้Q‡ๆQ…ไQƒแQR~S{ุTyิVvะZsฬ^pฦcnภgjธนhgฐhhณ~ฐqพsผ.qพะnมlฤjฦjศjษkสlสmหnหoหoสoษpศpวoฦoฤoรnมmฟlพlผkบjธiถhดgฒfฐfฎeญdซdฉcจbฆaฅ`ฃ`ข_ก^Ÿ]ž\œZ›YšX™๛W—๙V–๘U•๖T“๔S’๒R‘๑Q๏P์OŒ๊OŠ่NˆๆN†ใN„แO‚O€O}ืQzำSwฯVtส[qล`nฝckถžehฎdiฑoผpผoพฐlมiลhวhษhสjหkฬlอmอnฬoฬoฬpหpสoศoวoฦnฤnยmมlฟkฝjปiนiทhตgณfฒfฐeฎdฌcซcฉbจaฆ`ฅ_ค_ข]ก\ [žZYœW›Vš๚U™๙T—๗S–๕Q•๔P“๒O’๐N๎N์M๊L‹่LŠๅKˆใK†เL„LฺM~ึN|าPxฮSuษXrร]nป๘`kณydgฅaiฎmปoทlพ‚jม๛gลfศfสfหhอiอkฮlฮmฯnฮoอoอpฬoหoสoศnวnลmรlยkภkพjผiบhธgถgตfณeฑeฐdฎcฌbซbฉaจ`ง_ฅ^ค]ฃ\ข[ ZŸXžWUœ๛T›๚Sš๘Q˜๗P—๕O–๓N•๒M“๐L’๎K์J๊J็I‹ๅH‰โH‡เH…I‚ูI€ึK}ัMzอPwวUsมYoธ่[lฐJZpน\hซoผbยiพMgม๋eลdศcสdฬfฮhฯjฯkะlะmะnะoฯoฮoอoฬoหnษnศmฦlฤlยkภjฟiฝhปgนgธfถeดeฒdฑcฏcฎbฌaซaช`จ_ง^ฆ]ฅ[ฃZขYกW VŸUžS๛Rœ๚P›๘Oš๖N˜๕M—๓K–๑J•๏I”ํH’๋G้G็FๅE‹โE‰฿E‡F„ูFีHะJ{หNxลRtพVoตลWmฎWmฐgพhฝ eมวcลaศbหbอdฮeะhัjัkัlัnัnัoะoฯoฮoอnหnสmศlวkลkรjมiฟhพgผgบfธeทeตdณcฒcฑbฏbฎaฌ`ซ_ช^ฉ]จ\ง[ฅZคXคVฃUกT RŸQž๛O๙Nœ๘L›๖Kš๕J™๓I˜๑G–๏F•ํF”๋E’้D็CŽไCŒโB‹฿BˆB†ุCƒิE€ฯG}สKyฤPtปRpฒ‡VmฅToญeฟgฝdภ‹aฤ_ศ_ห`อbฯdะfัhาjำkำmำmาnาnัnะnฯnฮmฬmหlษkวjลjฤiยhภgพfฝfปeนdธdถcตcณbฒbฑaฏ`ฎ`ญ_ฌ^ซ]ช\จZงYฆWฅVคTฃSขQกP N ๚MŸ๙Kž๘J๖Hœ๔Gš๓F™๑E˜๏D—ํC•๋B”้A’็A‘ไ@โ@@Š@‡ืA„ำBฮE~ศIzมMtทๆQqฎ>Osฑafจjฟ๚_มbฟG`ร๊^ว]ส^อ`ฯbัdาfำhิjิkิlิmำnาnาnัmะmฮlอlหkสjศiฦhฤgยfมfฟeพeผdบcนcทbถbดbณaฒ`ฑ`ฐ_ฎ^ญ]ฌ\ซ[ชZฉXฉWงUฆSฆRฅPคOฃMขKก๚J ๙HŸ๗Gž๖E๔Dœ๓C›๑Bš๏A™ํ@—๋?–้?”ๆ>’ไ>‘แ=Ž=Œฺ>‰ึ>†า@ƒอCฦGzฝKuณงOpช Mrฎaภaพ_ยน\ฦ[ส\อ^ฯ`ัbาeิgิiีjีlีlีmิmำmาmัlะlฯkอjฬiสiศhวgลfรeมeภdพcฝcปbบbธaทaถaด`ณ`ฒ_ฑ^ฐ]ฏ\ฎ[ญZฌYซWชVชTจRจQงOฆNฅLคJฃHข๚Gข๙Eก๗D ๖CŸ๔Až๓@๑?œ๏>šํ>™๋=—้<–ๆ;”ใ;’แ;;ฺ;Šี<‡ะ>„สB€รFyน๐IuฑLHwดNoง`ฟ"]มl[ล๙YษYฬ[ฯ^ั`ำcิeีgึiึjึkึlีlีmิlำlาkัkฯjอiฬhสgศfวeลdฤdยcภbฟbฝbผaบaน`ธ`ท`ต_ด_ณ^ฒ]ฑ\ฑ[ฐZฏYฎXญVฌUซSซRชPฉNจLงKงIฆGฅEค๚Dฃ๙Cข๗Aก๖@ ๔?Ÿ๓>Ÿ๑=๏<œํ;›๋:™้9˜ๆ9–ใ8”เ8‘8ู9Œิ:ˆฯ=…ศAฟEyตซJtฉ Hvฎ\ม]ภ%ZฤาWศWฬYฯ[ั^ำaิcีfึhืiืjืkึkึlีlิkำkาjัiฯiฮhฬgสfษeวdลcฤbยbมaฟaพ`ผ`ป`บ_น_ธ_ท^ถ^ต]ด\ณ\ฒ[ฑYฐXฐWฏUฎTญRญQฌOซMชKชIฉGจFงDงCฆ๚Aฅ๙@ค๗>ฃ๖=ข๔<ก๓; ๑:Ÿ๏9žํ8๋8›้7™ๆ6˜ใ7•เ6“6ุ7ำ9‰อ<„ลA~บํEyฑBDzณ[ม_พ๚Yร„VฦUหVฮYั\ำ_ิaึdืfืhุiุjุkืkึkึkีjิjาiัhะgฮfฬeหdษcศbฦaฤaร`ม`ภ`พ_ฝ_ผ_ป^บ^น^ธ]ท]ถ\ต[ด[ดZณYฒWฑVฑTฐSฏQฎOฎNญLฌJฌHซFชDชCฉAจ@จ๚>ง๙=ฆ๘;ฅ๖:ค๕9ฃ๓8ข๑7ก๏7 ํ6ž๋5่4›ๆ4™ใ4—฿4”4‘ึ6ั8‰ส=ƒภB~ถ–KxฃE{ฎYยYม1Vล฿SษTอVะYา\ิ_ึbืdุfุhูiุjุjุjืjึjีiิhำgัfะeฮdอcหbษbศaฦ`ล`ร_ย_ภ^ฟ^พ^ฝ]ผ]บ]บ]น\ธ\ท[ถZตZตYดWดVณUฒSฒRฑPฐNฐMฏJฎIฎGญEญCฌAซ@ซ>ช=ฉ๚;ฉ๙:จ๘9ง๖8ฆ๕7ฅ๓6ค๑5ฃ๏4กํ3 ๋3ž่2œๅ2›โ1˜2•ฺ3’ี5Žฮ9‰ฦ>‚ปC}ณ&A~ตYย\ฟ๙Vฤ‘SศRฬSฯVาZิ]ึ`ืbุeูfูhูiูiูjุjืiึiีhิgำfัeะdฮcอbหaส`ศ_ว_ล^ฤ]ย]ม]ภ]ฟ\ฝ\ผ\ป\บ[บ[น[ธZทYทXถWถVตUดTดRณPณOฒMฒKฑIฑGฐEฐCฏBฎ@ฎ>ญ=ฌ;ฌ:ซ๛8ซ๚8ช๘6ฉ๗5จ๕4ง๓4ฆ๑3ฅ๏2ฃํ1ข๋1 ่0žๅ0œแ0™1–ุ2’ำ5ห:‡ภ๛@ถh;‡ผEwฐVรWย6SฦใPสQฮSัWิ[ึ^ืaุcูeฺgฺhฺiูiูiุiุhึgีgิeำeาdะbฯaอ`ห_ส^ศ^ว]ล\ฤ\ร\ย[ภ[ฟ[พ[ฝ[ผ[ปZปZบYนYนXธWธVทUทTถRถQตOดNดLดJณHณFฒDฒBฑ@ฑ?ฐ=ฐ;ฏ:ฏ8ฎ7ญ๛6ฌ๚5ซ๘4ซ๗3ช๕2ฉ๔1จ๒1ฆ๐0ฅํ/ฃ๋/ข่/Ÿไ/เ/š0–ึ2’ะ7Œฦ<…บฎD~ญ AณWม[ผ๙Sฤ’PษOอQะTำXี\ื_ุaูdฺeฺgฺhฺhฺhูhูhุgืfึeิdำcาbะaฮ`อ_ห^ส]ษ\ว[ฦ[ฤZรZยZมZภZฟZพZฝYผYผYปXปXบWนVนUธTธSทQทPทNถMถKถIตGดEดCดAณ?ณ>ณ<ฒ:ฒ9ฑ7ฑ6ฐ5ฏ๛4ฎ๚3ฎ๙2ญ๗1ฌ๖0ซ๔0ช๒/จ๐.งํ.ฅ๊.ฃ็- ใ.ž฿.šฺ0–ิ4‘ห9‰ฟใ@„ต*>…ธSรTย2QวแNหOฯQาUีYื]ุ_ูbฺdegghฺhฺgูgุfืeึdิcำbา`ะ_ฮ^อ]ฬ\ส[ษ[วZฦYลYฤYรYมXมXภXฟXพXฝXฝWผWผVปVปUบTบSนRนPธOธMธKธJทHทFทDถBต@ถ>ต<ด;ด9ด8ณ6ณ5ฒ4ฒ3ฑ2ฐ๛1ฐ๙0ฏ๘0ฎ๖/ญ๔.ซ๒.ช๐-จํ-ฆ๊,คๆ,กโ-ž.šุ1•ะ6Žฤ๙<ˆบ`:ˆผT›นVม๚aธ๐Rฤ‰NษMฮOัRิVึZุ]ู`ฺbdefgggฺfูfุeืdึcิaำ`า_ะ^ฯ\อ[ฬZสZษYศXฦXลXฤWรWยWมWภWภWฟWพWพVฝVฝUผUผTปSปRปQบOบNบLบKนIนFธDธCธAท?ธ=ท;ถ:ท8ถ7ถ5ต4ต3ด2ด1ณ0ณ๛0ฒ๙/ฑ๘.ฐ๖.ฎ๔-ญ๒-ซ๏,ฉ์,ง้+ฅๅ,ขแ-ž/šิ3“ษ:Œพ•V‚คC‰ทRฤSร๛'OฦูLหLฯOำSีWุ[ู^ฺacdfffffฺeูeุdืbึaิ`ำ_า]ะ\ฯ[อZฬYหXษWศWวVฦVลVฤVรVยVมVภVภVฟVฟUพUพTพTฝSฝRฝQผPผNปMปKปJปHบFปDบBน@น>บ<น:ธ9น7ธ6ธ5ท4ท3ท1ถ1ถ0ต/ด๛.ด๚.ณ๘-ฑ๖,ฐ๔,ฏ๒,ญ๏+ซ์+จ่+ฅไ,ข.ุ2—ฮ8ยฤ@Šธ>ŒปWร๙,ิPลuLษJฮMัPิTืXู\ฺ_acdefffeฺdูcุbืaึ`ิ^ำ]า\ะ[ฯYอXฬWหVษVศUวUฦUลUฤTรUรUยTมTมTภTภTฟTฟSฟSพRพQพPพOฝNฝLฝJฝIผGฝEผCผAป@ผ>ป<ป:บ8ป7บ6บ4น3น2น1ธ0ธ/ท/ท.ถ-ต๚-ด๘,ณ๖,ฒ๔+ฐ๑+ฎ๎+ซ๋+จๆ+ฅแ-ก0›า7”ฦๅ>Žฝ(<ภQฤ๚Sร๙MวศIหJะMำQึUุYฺ]`bceeeeedฺcูbุaื`ึ^ิ]ำ\าZะYฯXอWฬVหUสTษTศTวSฦSลSฤSรTรTยTยTมSมSภSภSภRภQฟPฟOฟNฟMฟKพJพHพFพDพBพAพ?พ=ฝ;ฝ9ผ8ฝ6ผ5ผ4ป2ผ2ป1บ0บ/บ/น.ธ-ธ-ท๚,ถ๘,ด๖+ณ๔+ฑ๑+ฎํ+ฌ้+จไ,ค/Ÿึ5˜ส๒=‘มI;“รTง๑MหOล๛WJษ๖HฮJาNีRืVูZ^`bcdeeedcbฺaุ`ื^ึ]ิ\ำZาYะWฯVฮUฬTหSสSษRศRวRฦRลRลRฤRรSรSยRยRยRมRมRมQมQมPภOภNภLภKภIภGภFภDฟBฟ@ฟ>ฟ=ฟ;ฟ9พ8พ6พ5พ4ฝ2พ1ฝ0ฝ0ฝ/ผ.ป.ป-บ-น,ธ๚,ท๘+ต๖+ด๓+ฑ๏+ฏ๋+ซ็,งแ.ขฺ3›ฮ;”รh9–ลRร๘Vภ๕ LฦจGหGะKำOึSุXฺ[^`bdddddcbฺaฺ`ุ^ื]ึ[ีZำYาWัVฯUฮTฬSหRสRษQศQวQวQฦQลQลQฤRฤQรRรRรQยQยQยPยPยOยNยMยLมKยIยHมFมDมBภAภ>ภ=ภ;ภ9ภ8ภ6ภ5ภ4ภ2ฟ1ฟ0ฟ0พ/พ.พ.ฝ-ผ-ป,บ,น๚+ธ๗+ถ๕+ด๒+ฑ๎+ฎ้,ชไ.ฅ3žั:—ล€6žฯ?ŒถNล๛Oฤ๚2IศๅEอGัKีPืTูY\_abcdddcbaฺ`ฺ_ุ]ื\ึZีYำWาVัTฯSฮRอQฬQหPสPษPศPวPวPฦPลQลQลQฤQฤQฤQฤQรPรPรPรOรNรMรLรKรIรGรFยDยBรAร?ร=ย;ย9ม8ม6ม5ม4ย2ม1ม1ภ0ภ/ภ.ฟ.ฟ-พ-พ,ฝ,ป๛+บ๙+ธ๗+ถ๔+ด๐+ฑ๋,ญๆ.จ฿2กิ:šศ“nx‘D”พUม๕7ะLฦ๛wFสEฯHำLึQุUฺZ]_ab฿c฿dccba`ฺ_ฺ]ุ\ืZึYิWำUาTัSฯRฮQอPฬPหOสOษOศOศOวOวPฦPฦPลPลPลPลPลPลPลPลOลOลNฤMฤLฤKฤIฤHลFลEลCฤAฤ?ฤ>ฤ<ร:ร8ร7ร5ฤ4ร3ร2ย1ร0ย/ย/ม.ม-ภ-ภ-ฟ,พ,ผ๛+ป๙+น๕+ถ๒+ณํ,ฐ่.ซแ2คื:ห O’ธD˜รNฤ๘Qร๗IวฝDฬDัHิMืRูVZ]`฿a฿b฿c฿c฿cba`_ฺ^ฺ\ุZืYึWิUำTาSัRฯPฮOอOฬNหNสNษNษNศNศNวOวOวOฦOฦPฦPฦPฦPฦPฦPฦOฦOฦNฦMฦLฦKฦJฦHฦGฦEฦCฦAฦ@ฦ>ล<ล:ฦ9ฦ7ฦ6ล4ล3ฤ2ฤ1ล0ฤ0ร/ร.ร.ย-ย-ภ,ภ,พ,ฝ๚+ป๗+น๔+ถ๏,ฒ๊.ญใ2ฆู:ŸอฉK˜พ C›ลJฦ๚Lล๙:Fษ์BฮDาIึNุSW[^฿`฿a฿b฿c฿c฿bb`_^ฺ\ฺ[ุYืWึUีTำRาQัPฯOฮNอNฬMฬMหMสMษMษMศMศNศNวOวOวOวOวPวPวPวOวOวOวNวMวMวKวJศIศGศFศDวBวAว?ศ=ศ;ว:ว8ว7ว5ว4ฦ3ว2ฦ1ฦ0ล/ล/ล.ฤ-ร-ร-ย,ภ,ฟ,ฝ๙,ป๕,ธ๑,ต์.ฏๅ2จ;กฯญK™ม DวQย๕>อJฦ๚xDสBะEิJืOูTX[^฿`฿aเbเb฿b฿aa`^][ฺZุXืVึTีRำQาPัOะNฮMอLอLฬLหLสLสLษLษMษMศNศNศNศOศOศOศPศPศPศOษOษOษNษMษLษKสJสHสGสEสCสAส@ษ>ษ<ษ:ษ9ศ7ศ6ษ5ษ3ศ2ศ1ศ1ว0ว/ฦ/ฦ.ล-ฤ-ร-ย,ม,ฟ๚,ผ๗,บ๓-ถํ.ฑๆ2ช<ฃัซM›ร EžษLฤ๖Pย๓ Gว๛ดAฬBัFีKุPฺUY\฿^เ`เaเbเbเa฿a฿`_]\ZูXุVืTึSีQำPาNัMะLฯLฮKอKฬKหKหKสKสLสLษMษMษNษNษOษOษOษPษPสPสPสOสOสOหNหMหLหKหIหHหFหEหCหAห?ห=ส<ห:ห8ห7ส6ส4ส3ษ2ษ1ษ0ษ0ศ/ว.ว-ฦ-ล-ฤ,ย,ม,พ๘,ผ๔-ธ๎.ณ็3ฌ<ฅำฃQœมGกสJล๗Kร๕)Dศใ@ฮBาGึLูQUY\฿_เ`เaเaเaเa฿`฿_^\ZXูVุUืSึQีPำNาMัLะKฯKฮJอJฬJฬJหJหKหKสLสLสMสNสNสOสOสPหPหPหPหPหPฬPฬOฬOฬNฬMฬLฬJอIอGอFอDฬBฬ@ฬ?อ=ฬ;ฬ:ฬ8ฬ7ฬ5ฬ4ห3ห2ห1ส0ส/ษ/ษ.ศ-ว-ฦ-ฤ,ย,ภ๙-ฝ๕-บ๐/ต่4ฎ>ฆำ˜^•ณHกษGฦ๘Iล๖YBส๗?ฯBำGืMฺRVZ฿\เ^เ`แ`แaเ`เ`เ_฿^\[YWฺUุSืQึPีNำMาLัKะJฯIฮIอIอIฬIฬJหJหKหKหLหMหMหNหOหOหPฬPฬPฬQฬQอQอQอPฮPฮOฮNฯMฯLฮJฮIฮGฮEฮDฯBฯ@ฮ>ฮ<ฮ;อ9อ8อ6อ5อ4ฬ2ฬ2ฬ1ห0ห/ส.ษ.ศ-ว-ล-ร-ม๚-พ๖-ป๐/ถ้5ฎ฿>จิˆ ฑ่LฅฬLฤ๐Fว๘Š@ห?ัCีHุNฺRVZ฿]เ^แ_แ`แ`แ`เ_เ^฿][YWUฺTุRืPึNีMิKาJาIะIะHฯHฮHอHอHอIฬJฬJฬKหLหLฬMฬNฬOฬPอPอQอQอQฮRฮQฮQฯQฯQฯPะOะNะMัLัJัIัGัEะCะAะ?ะ>ฯ<ะ:ฯ9ฯ7ฯ6ฯ5ฮ3ฮ2อ1อ0ฬ0ฬ/ห.ส.ศ-ว-ล-ย๛-ภ๗.ผ๑0ท๊7ฏ฿Aฉีs?ฉึ?ฮ์Jฤ๓Nย๐ Dว๙ถ>ฬ?ัCีIุNSV฿Zเ]แ^แ_แ`แ`แ_เ^เ]฿\ZXVTฺRุPืOึMีKิJำIาHัGะGฯGฮGฮGอGอHอIอJอKฬKฬLอNอNอOอPฮQฮQฮRฯRฯRะSะRัRัRัQัPาPาNาMาLาJาHาFาDาCาAา?ั=ั;ั:ั8ะ7ะ5ะ4ฯ3ฯ2ฮ1ฮ0อ/ฬ/ห.ส.ศ-ฦ-ฤ-ม๗.ฝ๒1ธ๊9ฐ฿๘Cชี[AซืGฤ๓Iร๑Bว๙=อ?าCึIูOSW฿Zเ]แ^แ_โ_แ_แ^แ]เ\฿[YWUSฺQูOืMึKีJิIำGาFัFะFฯFฯFฮFฮGฮHอHอIอJอKอMฮNฮOฮPฯPฯQฯRะSัSัSัTาSาSำSำRำRำQำPิNำMำKำJิHำFำDำBำ@ำ?ำ=า;า9า8า6ั5ั3ะ2ะ1ฯ0ฮ0อ/ฬ.ห.ษ.ว-ล-ย๘/พ๒2ธ้;ฑ๎Fชิ=CฌืFล๔Hฤ๒;@ศ๚๎<ฮ?ำDืJฺOSW฿Zเ\แ^โ^โ_โ^แ^แ\เ[฿Y฿WUSQฺOูMุLึJีHิGำFาEัEัEะEฯEฯFฯFฮGฮHฮIฮJฮKฯMฯNฯOะPะQะRัSัSาTาTำUำUิUิTิTีSีRีQีPีNีMีKีIีGีEีCิAิ@ิ>ิ<ำ:ำ8ำ7ำ6า4า3ั2ะ1ะ0ฯ/อ/ฬ.ส.ศ.ฦ.ย๘/พ๒3น้=ฑ฿Hฌี EฎืDล๔Fฤ๒\>ษ๚๘<ฯ?ำDืJฺOSWเZแ\แ]โ^โ^โ^แ]แ\เZ฿X฿VTRPฺNูLุJืHีGิFิEำDาDัDะDะDะEฯFฯGฯHฯIฯJฯKฯMะNะOัQัRาSาTำTำUิUิVีVีVีUึUึTึSืRืQืPืNืLึKึIึFึEึCึAี?ี=ี;ี9ิ8ิ7ิ5ำ4า2า1ั0ะ0ฯ/อ.ห.ษ.ฦ.ร๘0ฟ๑5น่@ฑพMซาHฎึEฟ๔Aฦ๔Cฤ๒w=ส๛;ะ?ิDืJฺOSWเZแ[โ]โ]โ]โ]โ\แ[เY฿WUSQOฺLูJุIืGึEีDิCำCาCาCัCัDะDะEะFะHะIะJะLัMัNัPาQาRำTำUิUิVีWึWึWืWืWืVุVุUุTุRุQุOุNุLุJุHืFืDืBื@ึ>ึ<ึ:ึ9ี7ี6ิ4ิ3ำ2า1ั0ะ/ฮ/ฬ.ส.ว/ร๘1ฟ๑8ธ็Bฒ•mกฦNญืLภ้Bล๒Ž<ห๛;ั?ีEุJOS฿VเYแ[โ\โ\โ\โ\โ[แZเXเV฿SQOMฺKูIุGืEึDีCิBำBำBาBาBัCัDัEัFัHัIัKัLาNาOาQำRำSิTีUีVึWืXืXืXุXุXูXูWูVูUฺTฺRฺQูOูMูKูIูGูEุCุAุ?ุ=ื;ื9ึ8ึ6ี5ี4ิ2ำ1า0ั0ฯ/อ/ห/ศ0ฤ๘2ฟ๐:ธๅFฑhCตH‹็Kม๋UผโAล๒ก;ห๛:ั?ีDุJOS฿VเYแZโ[โ\โ[โ[โZแXแW฿U฿RPNLฺJูHุFืDึCีBีAิAำAำAาBาBาCาEัFาHาJาKาMำNำPิQิSีTึUึVืWืXุYุYูZูZฺZฺYXWVUSRPNLฺJฺHฺFฺDูBู@ู>ุ<ุ:ื9ื7ึ6ึ4ี3ิ2ำ1า0ะ0ฮ/ห/ศ0ฤ๗4พ๏>ทไ์Iฑฺ8FณGย๋Kฟๆ @ฦ๒ฏ:ฬ๛:ั>ึDูINR฿UเXโZโ[โ[โZโZโYแWแU฿S฿QOMJHฺFูDุCืAึ@ี@ิ@ิ@ำ@ำAำBาCาEาFำHำJำLิNิOิQีSึTึUืWืXุYูZูZฺ[ฺ[[[ZZYWVTSQOMKIGECฺAฺ?ู=ู;ู9ุ8ื6ื5ึ3ี2ิ1า0ั0ฮ0ห0ศ1ฤ๖6พ๎BทโสMฒูJณFย๊Iฟ็ ?ฦ๑ท9ฬ๚9า>ึCูHMQ฿UแWโYโZใZโZโYโXแVแTเR฿PNKIGฺEูCุAื@ึ?ี?ี?ิ?ิ@ิ@ำBิCิEำFิHิJิLีNีPึRืTืUุWุXูYฺZฺ[\\\\\[[ZYWVTRPOLJHECA@>ฺ<ฺ:ู8ุ7ุ5ื4ึ3ี2ำ1ั0ฯ0ฬ0ศ3ร๕:ฝ๋Eถแ“nขฦPฑฺEม้Iฟๆ>ล๐ผ8ฬ๙8า=ึBูHMPเSแVโXใYใYใYใXโVแUเSเQ฿OLJHEฺCูAุ@ุ?ื>ึ>ี>ี>ี?ี@ิAิCิDิFีIีKีMึOึQืSุUุWูXฺZ[\]]^^]]\฿\฿[฿Z฿X฿W฿U฿SQOMKIFDB@><:ฺ9ู7ุ6ุ4ื3ี2ิ1า1ฯ0ฬ1ศ๛4ร๓=ผ้๗Hถ฿WEธแห%zDม้Hฟๅ>ล๏ป8ห๘8ั<ึAูFKOเRแUโVใWใXใWใVโUโSแRเP฿MKIFDฺBู@ู?ุ>ื=ื=ึ=ึ>ี>ี@ีAีCีEีGึIึLืNืPุRุTูVฺXZ[]^^__฿_฿_เ^เ^เ]เ\เ[เYเXเVเTเR฿P฿N฿L฿IGECA?=;9ฺ8ู6ุ5ื4ึ2ิ2า1ฯ1ฬ2ว๚6ย๑BปๆฺMถ"Jท฿Eม่Iฟไ =ล๎ต7ห๗7ั:ี@ูEJNเQแTโUใVใVใVใUใTโRแPเN฿LJGEBAฺ?ู>ุ=ุ<ื<ื<ึ=ึ>ึ?ึAึCึEืHืJืMุOุQูSฺVXZ\]^_฿`฿aเaเaแ`แ`แ_แ^แ]แ\แZแXแVแUแRแPเOเLเJ฿G฿E฿CA?=;:8ฺ7ู5ุ4ึ3ี2า1ฯ2ห4ว๘:ภ๏Eบไ [ฑีPถDภๆJฝแ =ฤ์ซ7ส๕6ะ9ี>ูCHMเPแRโTใUใUใTใTใRโPแOแMเJ฿HFCA?ฺ>ฺ<ู<ุ;ุ;ื<ื<ื>ื?ืAืCืFุHุKูNูPฺRUWZ\]฿_฿`เaเbแbโcโcโbโaใaใ`ใ^ใ]ใ[ใYโWโUโSโQแOแMแKเHเFเD฿B฿@><:87ฺ6ุ4ื3ี2า2ฯ2ห6ฦ๖>ฟ๋๗IนโZEปๆ[ฎัEภใSบุ=ร๊›6ษ๓5ะ8ิ=ุBFJ฿NแPโRใSใSใSใRใQโOโMแKเI฿FDB@>=ฺ;ู:ู:ุ:ุ;ุ<ุ>ุ?ุAุCุFูIูLฺOQTVY[฿]฿_เaแbโcโcใdใdไdไdไcๅbๅaไ`ไ^ไ\ไZไXใVใTใQใOโMโKแIแFแDเB฿@฿><:97ฺ6ู5ื4ี3า2ฯ4ส8ล๔Cฝ่ีNธ฿KนโDผ1ห๗=ม็†6ศ๑4ฯ๚7ิ;ุ@ฺDI฿LเNโPใQใRใQใPใOใMโLแJเGเE฿CA?=;ฺ:ฺ9ู9ู:ู:ู;ุ=ุ?ูAูDฺFฺJMPRUX[฿]เ_แaโbใdใeไeๅfๅfๆfๅeๆeๆdๆbๆaๆ_ๆ^ๅ[ๅYๅWไTไRไPใNใKโIโGแDแBเAเ?฿=;986ู5ุ4ี3า4ฮ5ษ๚<ร๐Gผๆ‘gฎะQธ฿uxต<ย็>ภๅk7ฦ๎3อ๘5ำ9ื>ฺBFIเLแNโOใPใOใOใMใLโJโHแFเC฿A?=;:9ฺ9ฺ9ฺ9ู:ฺ;ฺ=ู?ฺAฺDGJNPSW฿Zเ\แ_โaใbไdไfๅgๆgๆh็h็h่g็f็e็d็b็`็_็]ๆZๆXๅUๅSๅQไNไLใJใGโEโCแAเ?เ=฿;987ฺ5ุ4ี4า5อ8ศ๗Aม์๐KปใEIผๅ=ภๅ?ฟใJ7ล๋๓2ฬ๕3ั7ึ;ู@CG฿IแLโMใMใMใMใLใJโHโFแDเBเ@฿><:988889ฺ;ฺ=ฺ@BEHKOQ฿UเXเ[แ^โ`ไbไdๅfๆh็i่i่j้j้j้i้h้g้e้d้b่`่^่\็Y็VๆTๆQๅOๅMไJไGใEโCโAแ?เ=฿;:87ฺ5ื4ี4ั6ฬ;ฦ๔Fฟ้บRน฿Nผใ>พโ@ฝเ'8ร่ไ2ส๒2ะ๚5ิ9ุ=AD฿GเIโJโKใKใJใIใHใFโDโBแ@เ>฿<฿:9877789;=?BEHLO฿SเVแYโ\ใ_ไbๅdๆf็h่i้j้k๊l๊l๋k๊k๋j๋h๊g๊e๊c๊a้_้]้Z่W็U็RๆOๆMๅKๅHไEใCใAโ?แ=เ;฿:87ฺ6ื5ิ5ะ8ห๚@ฤ๐๚Jพๆh>ยํUผใ@ผ฿Cบ9มๅย2ศ๏0ฮ๗3ำ6ื:ฺ>ADเFแGโHใHใHใGใFใDโBโAแ?แ=เ;฿9฿8766679:=?BEI฿M฿PเSแWใ[ใ^ๅ`ๆc็e่h้j้k๊l๋m๋m์m์m์l์k์j์h์f์d๋b๋`๊^๊[้X้U่S่P็NๆKๅHๅFไCใAโ?แ=เ;฿:87ฺ6ื5ำ6ฯ:ษ๗Eย์ิPฝโ!MพๅEธูgฃน;พแ‘3ล๋0ฬ๕1ั๛4ี8ู;>A฿CเDแEโEใEใDใDใBโAโ?โ=แ;แ9เ8฿7฿6฿55568:แ<฿:87ู6ึ6ำ8อ?ฦ๓Iฟ้„|ฃมSบโ“}€;พแ=ผ[5ย็๘0ส๑0ะ๘2ิ5ื8ฺ;>@เAแBโBโBใBใAใ@ใ?โ=โ;แ:แ8แ7เ6เ5฿4฿4฿5฿6฿7฿9฿<฿?฿BเFเJแNโQใUไYๅ]ๆ`่c้e๊h๋j์lํn๎o๎p๏p๏p๐p๏o๐n๐l๏k๏i๏g๎dํbํ`ํ]์Z๋W๊T๊R้O่L็J็GๆEๅBใ@โ>แ<฿:87ู7ึ7ั;ห๚Dฤ๏โNพๅ1Lภ่=ผ?ป%7ภใ0ศํ/ฮ๕0า๛2ึ5ู8;=฿?เ?แ@โ@โ?ใ?ใ>ใ=โ;โ:โ8โ7แ6แ4แ4เ3เ3เ4เ5เ7เ9เ;เ?แBแFโJใNใQไUๅZๆ]่a้d๊f๋i์lํn๎o๏p๐q๐q๑q๑q๑p๑o๑n๑l๑j๐h๐e๏c๎`๎^ํ[์X์U๋R๊P้M่J็H็EๅBไ@ใ?แ<เ:88ุ7ี9ะ?ษ๕Iย๋–]ทุQฝใ?ทฺEฒี9ฝ฿ฆ1ฤ่.ห๒/ะ๘0ิ3ุ6ฺ8:;฿=เ=แ=โ=โ<โ;ใ:ใ9โ8โ7โ5โ4โ3แ3แ2แ2แ3แ4แ6แ8แ;แ>โAใEใJไNๅQๆV็Z่^้a๊d๋gํj๎m๏n๐p๑q๑r๒r๒r๓r๓q๓p๓o๒m๒k๑i๑f๐d๐a๏_๎\ํYํV์S๋P๊N้K่H็EๆCๅAใ>โ=เ:98ื8ำ<อ๛Eฦ๑่Nภ่;Lย๊=นิ9ผ;บ^4มไ๘.ษ๎-ฮ๕/ำ๛1ึ3ู578฿9เ:แ:โ:โ:โ9โ8ใ7ใ6ใ5โ4โ3โ2โ2โ1โ2โ2โ4โ6โ8โ:ใ>ใAไEๅIๅMๆQ็V่Z้^๋a์eํh๎k๏m๐o๑q๒r๒s๓s๔s๔s๔r๔q๔p๔n๓l๓j๒g๑e๑b๐_๏]๏Z๎WํS์Q๋N๊L้I่FๆCๅAไ?โ=เ;9ฺ9ึ:า@ห๗Iฤ๎XฝใQภ่=นู?ทึ7ฝ฿ำ/ล้-ฬ๒-ั๘/ี0ุ2ฺ467฿7เ8แ8โ8โ7โ6ใ6ใ5ใ3ใ3ใ2ใ1ใ1โ1โ1โ2โ3ใ5ใ7ใ9ไ=ไ@ๅDๆHๆM็Q้U๊Y๋]์aํe๎h๏k๑n๑p๒q๓r๔s๔t๕t๕t๕s๕r๕p๕o๕m๔k๓h๓e๒c๑`๑]๐[๏W๎TํQ์O๋L๊I่F็DๆAไ?โ=฿;:ู:ี=ฯEศ๒้Oย้=Mร๋Aดึ\ ฤ:บˆ2มไ-ษ๎,ฯ๕-ำ๚.ื0ู134฿5เ5แ5แ5โ5โ4ใ3ใ3ใ2ใ1ใ1ใ0ใ0ใ0ใ0ไ1ใ2ไ4ไ6ไ9ๅ<ๅ@ๆC็G่L้P๊T๋Y์]ํa๎d๐g๑k๒m๓p๔q๔r๕t๖t๖t๖t๖s๗r๖q๖o๖n๕k๕i๔f๓d๒a๒^๑[๐X๏U๎RํO์L๊I้G่DๆAไ@แ=฿;;ุ<ำAฬ๘Kฦ๏™ZฝใRย้;ธุ=ทื65ฝๆ.ฦ้,ฬ๑,ั๗-ี.ุ/012฿3เ3แ3โ3โ2โ2ใ1ใ1ใ0ไ0ไ/ไ/ไ/ไ0ไ0ไ2ๅ3ๅ6ๅ8ๆ;ๆ?็B่F้K๊O๋S์Wํ\๎`๐c๑f๒j๓m๔o๕q๖r๖s๗t๗t๗t๗t๘s๗q๗p๗n๖l๖j๕g๔d๔a๓^๒\๑Y๐U๏RํP์M๋J้G็DๅBไ@แ>฿<<ื?ัGษ๔โPล๋5Nฦํ?ดิEฏฮ8นูœ1มใ,ษํ+ฯ๔+ิ๙,ื-ฺ./0฿0เ1แ1แ1โ1ใ0ใ0ใ0ไ/ไ/ไ.ไ.ไ.ๅ/ๅ0ๅ1ๅ2ๆ4ๆ7็:็=่A้E๊I๋N์RํV๎Z๏_๐b๒e๓i๔l๕n๖p๗r๗s๘t๘t๙t๙s๙s๘q๘p๘o๗m๗j๖g๕d๕b๔_๓\๒Y๑V๏S๎PํM๋J๊G่DๆBใ@แ>=ฺ>ีDอ๙Mฦ๐†`ธเSม๊:ทื<ถี>4ผ๊.ฦ่+อ๑*า๗+ึ+ู,-./฿/เ/แ/โ/โ/ใ/ใ.ไ.ไ.ๅ.ๅ-ๅ.ๅ.ๆ/ๆ0ๆ1ๆ3็6็9่<้@๊C๊H๋L์P๎T๏Y๐]๑a๓d๔g๕k๖m๗o๘q๘r๙s๙s๚s๚s๚r๙q๙p๙o๘m๘j๗g๖e๕b๔_๓\๓Y๑V๐S๏PํM์K๊G่EๆBใ@เ?>ุAาIส๔ะRฤ๋%Pฦ๎>ฒำDญอ8ธุ›1มโ+ส์*ะ๔*ี๙*ุ+,,-฿-เ.แ.โ.โ.ใ.ใ.ไ-ไ-ๅ-ๅ-ๆ-ๆ-ๆ.็/็1็2่5่7้:้>๊B๋E์JํN๎S๐W๑[๒_๓b๕f๖i๗k๘n๙p๙q๚r๚r๛s๛r๛r๚q๚p๚n๙m๙j๘g๗e๖b๕_๔]๓Z๒V๑S๏P๎M์K๊H่EๅBโ@฿?@ึFฮ๘๗Nศ๏eํUร๊:ถี<ดา74ปใ-ฦ็*ฮ๐)ำ๖)ื๛)ฺ*+,,฿-เ-แ-โ-ใ-ใ-ไ-ๅ-ๅ,ๆ-ๆ-ๆ-็.็/็0่1่3้6๊9๊<๋@์CํH๎L๏P๐T๑X๓]๔`๕c๖g๗j๘l๙n๚p๛p๛q๛q๛qq๛p๛o๛n๚l๙j๙g๘e๗b๖`๕]๔Z๓V๑S๐P๎N์K๊H่DๅBแAAูEาMห๓ฌUฤ๊Rว๎=ฒฯFฉย9ทึ†1ภแ+ส์)า๓(ึ๙(ู)**+฿+เ,แ,โ-ใ,ใ-ไ,ๅ,ๅ,ๆ,ๆ-็-็-่.่/้0้2๊4๊7๋:์>์AํE๎I๏N๑R๒V๓Z๔^๕a๗d๘g๙j๚l๚n๛opppponm๛k๚j๙g๙e๘b๗_๖]๔Z๓V๒S๐Q๎N์K้H็DไBเBDึJฮ๗฿Rศ๏8Nษ๓jษุ;ติ=ณั"6ปฺห.ฦๅ)ฯ๏'ี๖'ุ๚(ฺ()*฿+เ+แ,โ,โ,ใ,ไ,ๅ,ๅ,ๆ,ๆ,็-่-่.้/้0๊1๊3๋6๋8์;ํ?๎B๏G๐K๑O๒S๓W๕[๖^๗a๘d๙g๚j๛kmnooonmlk๛i๚g๙d๘b๗_๖]๕Z๓V๒S๐P๎M๋J้GๆDโCDูIั๚๘Oห๒qbตืSฦ์>ตา/ป฿:ถีU3ฟ๏*ส๊'า๒&ื๘'ฺ(()*฿+เ+แ,โ,ใ,ไ,ๅ,ๅ,ๆ,็-็-่-่-้.้/๊0๋2๋4์7ํ:ํ=๎@๏D๐H๑L๒P๔T๕W๖[๗^๘b๙d๚g๛iklmmmmlkjh๛f๚d๙a๘_๗\๕Y๔V๒S๐PํM๋J่GไEแDHีNฮ๖ฆUว๎Sส๑?ฒัFซส8ธืŠ/ฤโ(ฯํ%ี๔&ุ๙''()฿*เ+แ,โ,ใ-ไ-ๅ-ๅ-ๆ-็-็-่-้.้.๊/๋0๋1์3์5ํ8๎;๏>๐A๑E๑I๓M๔P๕T๖X๗[๘^๙a๚d๛fhijklkkjhge๛c๚`๙^๗[๖X๔U๒R๏OํL๊IๆGใFGุMั๘ฮSฬ๑*Qอ๓>>>>;ตำ>ฒะ5ผฺฒ+ศๅ%า๏$ื๖%ฺ๚'((*เ+แ,โ-โ-ไ-ไ-ๅ-ๆ-็-่.่.้.๊.๊/๋/๋1์2ํ4ํ6๎9๏;๐?๑B๒F๓I๔M๕Q๖T๗X๘[๙^๚`๛cefhiiiihgfd๛b๚`๙]๗Z๖X๔T๒Q๏N์L่IไGเHMำ๚ๆSอ๓KGฺVส๏yŒˆˆ{ƒo—‹ jŸ!kข’Fjฅ“giง•}hจ–‹gซ—Ždฌ˜‡bฎ˜wbฏš\bฐœ8cฏ›wญœpฎœIกร6พฺ<ถำ,2ภห(ฬ่#ิ๑$ุ๗&๛'()฿+เ,แ-โ.ใ.ไ.ๅ/ๆ.็.่.่.้/๊/๊/๋0์0์2ํ3๎5๎7๏9๐<๑@๒C๓F๔J๕M๖Q๗T๘W๙[๚]๛`bceffggfedb๛`๚^๙\๗Y๕V๓S๑P๎M๊J็IโHLี๒Rฯ๕j^ภVห๎b™‰e•†_Š&]ขl\ฅฎ\จ’Zซ”๑Yญ–Wฏ—Tฑ˜QดšLตšHถ›Bธ›๘?ธœ์>ธœฯ=ธœ˜?ธ›UCถ˜oจ–Xฏ—?ญห&ึ๏8ธิ>/ร฿ื%ฯ๊#ึ๒%ู๗&')฿+เ-แ.โ.ใ/ไ/ๅ/ๆ0ๆ0็0่/้/๊0๋0๋0์1ํ1ํ3๎4๏6๐8๐:๑=๒@๓C๔G๕J๖M๗Q๘T๙W๚Z๛\^`bcddddcb`๛_๚]๘Z๗W๕T๒Q๐O์L้JไJ฿Lุ๘Qั๗ƒZว์Vห๑Q—M‰uSž‡)Tฃ‹‰SงRซ’๛Qฏ•Qฒ—Pด™PตšOถ›MธœJบEปž>ผž7พŸ/ฟŸ'ฟŸ#ฟž#ฟž๖$พฯ&ฝ*ปœ01ถ™3ธฌ6ปึF,ฦเฺ#ั๋#ื๓%ฺ๘'(*฿-แ/โ0ใ0ไ0ๅ1ๅ1ๆ1็1่0้0๊0๋0๋1์1ํ2ํ2๎4๏5๐7๑9๑;๒>๓A๔D๕G๖J๗N๘Q๙T๚V๛Y๛[]_`aabaa`^๛]๙Z๘X๖U๔R๑P๎M๊KๆJเMฺ๚Qำ๙‘Wฬ๓Uฯ๕IŸ…Hž„ LฃˆhMจŒืMญ‘Lฒ•Lด—Kต˜Kถ™Kท›KธœKบKปžJผŸHฝ Dฟก>ภก7มข.ยข%รขฤขฤกร ยŸโภž–ฟž<บœ4พุD)ษแิ#า๋$ุ๓&๘(*฿.เ0แ2โ2ใ2ไ2ๅ2ๆ2็2่2้2๊2๋2๋2์2ํ2๎3๎3๏5๐6๑8๑:๒<๓?๔A๕D๖G๖K๗M๘P๙S๚U๛XZ\]^___^]๛\๚Z๘X๗V๕S๒P๏N๋L็KโM๚Rิ๙–Vฮ๒Uั๕BŠrIณ”EžƒGฆ‰›IญŽ๕Hฒ“Gด•Dต—Cถ˜Bท™BนšCบ›DปEผžFฝŸFพกFฟขEมฃBยฃ>รค9ลค1ฦฅ(วค ศคศคศฃวขลก้ฤ กยŸCป™ 3ม5'หใร"ำ๋%ู๓'๘)-฿1แ3โ4ใ4ไ4ๅ4ๆ4็3็3่3้3๊3๋3์3ํ3ํ3๎4๏5๏6๐7๑9๒;๓=๓?๔B๕E๖H๗K๘M๙P๙R๚U๛WXZ[\\\[๛Z๚Y๙W๗U๕S๓Q๐N์L่KใN๗Rึ๙Vะ๑Tา๔;˜wJถ›?ก‚,Cฉ‰นEฐDด“@ต•<ถ–:ท—:ธ˜:บ™;ป›<ผœ=ฝž?พŸ@ภ BมขBยฃCรคBฤฅAลฅ>ฦฆ:วง3ษง+สง"หงหฆหฆหฅสคศฃํวขฉล K(รส4%อใฆ"ิ๋๘%ู๒(๘+1เ5โ7ใ7ใ7ไ6ๅ6ๆ6็5่5้5๊4๋4์4์4ํ4๎4๏5๏6๐7๑8๑9๒;๓>๔@๕B๖E๖H๗J๘M๙O๙Q๚S๛U๛V๛X๛X๛Y๛X๚X๙W๘V๗T๕R๒P๐N์L่LใN๐Rึ๙~Yั๔ Vำ๖9—zCท=ฃƒ0Aซ‰ฤCฑ?ด’:ต“6ท”4ธ–3น—4ป™5ผš6ฝœ7ฟ9ภŸ;ม =ยก>รฃ?ฤค@ลฅAฦฆ@วง?ศจ=ษจ:สจ4หฉ.ฬฉ&อฉฮฉฮฉฯจฮงฮฆฬฅ๑สฃฒศญj อู’"ิ๊่&ฺ๒)๗.฿5แ9โ:ใ:ใ9ไ9ๅ8ๆ8็7่6้6๊6๋5์5ํ5ํ5๎5๏6๐7๐8๑9๒:๓<๓>๔@๕C๖E๖G๗J๘L๘N๙P๙R๚S๚T๚T๚U๙U๘T๗S๖R๔P๒O๏M์L็MโO฿Sึ๙aZฯ๑Vา๕/‹n?ฐŒ9ค‚*?ฌŠร@ณ;ต‘6ถ’2ท“0น•0บ–0ผ˜0ฝš2พ›3ฟ5มŸ6ย 8รก:ฤข<ลค=ฦฅ>วฆ?ศง?ษจ?สฉ>หฉ<ฬช9อช5ฮซ0ฯซ*ะฌ#ัซัซาซาชาฉัฉะง๔ฯจฤฯมฐิแ฿%ู๐*๖2เ๛9โ<โ<โ<ใ<ใ;ไ:ๆ9็9่8้7๊7๋6์6ํ6๎6๎6๏7๐8๑8๑:๒;๓=๓?๔A๕C๖E๖G๗I๗K๘M๘N๘O๙P๘Q๘Q๗Q๖P๕P๓N๑M๎L๋LๆMเPพTึ๙=eภ์Xั๗9จƒ7ฆ‚=ญˆถ>ณŽ8ต1ถ‘.ธ’-น”,ป–-ฝ˜.พ™/ฟ›0ม1ยž3รŸ5ฤก7ลข8ฦค:วฅ<ศฆ=ษจ>สฉ?หฉ?ฬช>อซ>ฮซ<ฯฌ9ฯฌ5ะญ1ัญ,าญ'ำฎ"ิญีญีญึญึฌึฌิซ๗ำฒ้ิฬ๖"ู็,๕6เ๚<แ>แ>แ>แ>โ=ไ=ๅ;็:่9้8๊8๋7์7ํ7ํ7๎7๏8๐8๐9๑:๒;๒=๓?๔@๔B๕D๕F๖H๖I๖K๖L๖M๖M๕M๔M๓M๑L๏K์K่LใN๊Qฺ๛‡Vึ๘=๋]ะํ6จ‚3ฃ~ ;ฎ‡˜;ด4ถ.ท+ธ’)บ”)ผ•*ฝ—+ฟ™,ภ›-มœ/รž0ฤŸ2ล 4ฦข6วฃ8ศฅ9ษฆ;สง=หฉ>ฬช?อซ?ฮฌ?ฯฌ?ะญ>ัญ<ัฎ9าฎ6ำฏ3ิฏ/ิฏ+ีฐ'ึฐ"ืฐุฐูฐูฑฺฑฺฐูฑูพฺุ*๎8เ๘?เ@฿@?฿?แ?โ>ไ=ๆ<็;่:้9๊8๋8์7ํ7๎8๎8๏9๐9๐:๑<๑=๒?๓@๓B๓C๔D๔F๔G๓H๓I๓I๑I๐I๎I์I้JๅLเ๖OตRุ๙EXะ๑Uิ๕5ฃ}J฿ฎ8ญ†a9ณ‹๕1ถ*ท'น'บ’'ผ•(พ–)ฟ˜*มš+ยœ-ร.ฤŸ/ฦ 1วก3ศฃ5ษค7สฆ9หง;ฬฉ=อช>ฮซ?ฯฌ@ะญ@ัฎ@าฏ?ำฏ>ำฐ<ิฐ:ีฐ7ีฑ5ึฑ2ืฑ.ืฒ*ุฒ&ูฒ#ฺณดดตตตธว$฿฿6฿๑?๗@ู๘@ฺ๛@@?แ?ใ>ไ=ๅ;็:่9้8๊8๋8์8์8ํ8๎9๎:๏;๏<๐=๐>๐?๑A๑B๑C๐D๐D๏E๎F์F๊F่HไJเ๘MฤQู๙`Uี๖:์]อ๊3ฌƒ3ช6ฑˆศ0ถ‹'ท#น#ป‘$ฝ”&พ–'ภ—(ม™)ร›+ฤœ,ลž.ฦŸ/วก0ศข2ษค5หฅ7ฬง9อจ;ฮช=ฯซ?ะฌ@ัฎAาฎAำฏAิฐAิฑ@ีฒ?ึฒ=ืฒ<ืฒ9ุณ7ุณ4ูณ1ฺด.ด+ต(ถ%ท"฿ท เธแนโบโบใพโห0๙@า์ํBะ๐Aี๕Aู๚@@?เ>โ=ใ<ๅ;ๆ9็8่8้8๊8๋8๋8์9์:์:์;ํ<ํ=ํ>์?์@๋A๋B้C่DๅFโI฿๑LบOฺ๚aTื๗cรโXั๑+ฎ+ฌ"(ฑ…"ต‰ธŒบ ฝ’"ฟ”$ภ–&ย˜'รš)ฤœ*ล+วŸ-ศ /ษข0สฃ2หค4ฬฆ7อจ9ฯฉ<ะซ>ัฌ@าฎAำฏBิฐCีฑCึฒCืณCืณBุด@ุด?ูต=ูด;ฺต9ต7ต4ถ2ท/ท-฿ธ*เน(แบ&ใป$ไฝ$ๅพ#ๆพ#ๆฝ$โฝล>ฬูDEวๅCห้ฬBฯ๎๓Aา๓@ึ๘@ฺ๛?>=เ<โ:ใ9ไ8ๅ8ๆ8็8็8่8่9่:่:้;่<่=็>็@ๅAไCแF฿๗IืKฺ๚•Oื๘HRี๕nภ๋Zฯ๓!ฌ!ฉ|ฏ‚Zณ†สทŠ๙ปŽฝ‘ฟ”!ม–#ร˜%ฤš'ลœ(วž*ศŸ,ษก.สข0หฃ1ฬฅ4อง7ฯจ9ะช<ัฌ?ำฎAิฏBีฐDึฑDืณEุดEูดEูตEฺถCถBทAถ@ท>ท<ธ:ธ8฿น6เบ4แบ2โป0ใผ.ไฝ.ๅพ-ๆฟ-็ฟ,ๅฝ๗,เธg.์MณัGร฿FวใNDษ็Cฬ๋หBฯ๏๎Aา๓?ี๗>ุ๚=<:9฿9เ8แ8โ8ใ8ใ9ใ:ใ;ใ<โ>แ?เBD๎G๛ฮIฺ๙–Lื๘XOี๕Vั๏Sา๒-ฐ…4ญ‚)ฑ…&ต‰g"ธŒมปŽ๖พ’ม•ย—!ฤ™#ฦ›%ว'ษŸ)ส ,หข.ฬค1ฮฆ3ฯง7ะฉ:ัซ=ำญ@ิฏBึฐDืฑEืณGูดHฺตHฺถHทHธHธGนEนDนBนA฿บ?เป>แป<โผ:ใฝ9ไพ8ๅฟ8ๆภ8็ม9่ม9็ฟ7แน 7ุฑ6ดLภNพุHรเFวๅ=Dษ็pCฬ๋Bฮ๎ฦAะ๐ใ@ำ๓๏?ี๖๗=ึ๗=ุ๙<ุ๚<ฺ๛<ฺ๛=๛=๛>๛๗@ฺ๚๏Cู๙โDุ๙ยFื๘™Iื๗lKี๕ฒˆXช‚3ตŠ+ธŒX&ปณ!พ‘๐ม”ร—ล™ ว›"ศž%ส (หก+อฃ.ฮฅ1ะง5ัฉ9ำซ=ิญ@ึฐCืฑEุณGฺดIถJทKธLนLบL฿ปK฿ปJเปIเผHแผFโฝEโพDใพCไฟBๅภB็มB่ยD๊รF๋ลF๊รBไฝผ?ถ!@฿ธ`ชฑLฝูGฤโFวๆ$Eส่?Eฬ๋XDอํlCฮ๎zCฯ๏‚Bั๑…Cั๑ƒDา๒{Eา๒mGา๑YIา๑?Jำ๒#Mั๏Uห่AโJฎ‹69ต0บJ+ฝ‘ฅ%ฟ“๊ย–ล™ว›ษž!ห %ฬข(ฮค-ะฆ1าฉ7ิซ;ีฎ@ืฐCูฒFฺดIถKทMนO฿บOเปPแผPแฝPโพOใพNใฟNไฟMๅภLๆมL็ยK่รL้ฤM๋ฦPํวS๎ษQ์วJๅภวEธ.F่รFะซl+%ฟ—;ถŒ 3บ=.ฝ’˜(ม•ไ"ฤ˜ว›ษห !อข%ฯฅ+ัง0ำช7ีญ=ุฐCฺฒGตKทN฿นPเบRแผSใพUใฟUไภVๅภUๆมU็ยU่รU้ฤU๊ลV๋วWํศY๏ส\๑ฬ]๑อX๎ษOๆมรHน2P๑หEิฑ:ถŽ>ณ6ป‘21พ”‹+ย—%ลšษหŸฮข"ะฅ'าจ/ีซ6ืฎ>ฺฑDดJธNเบRโผUไพXๅภZ็ย[่ร]้ฤ]๊ล^๋ว_ํศ`๎สb๐ฬd๒ฮf๓ะg๔ัd๓ฯ[ํษ๛PไภฐIท(V๗าEัฌ=ทCฒ‹6ผ’)2ฟ•-ร˜ี'ว›๛!หŸฮขะฅ$ำง+ีซ4ุฎ<ฒDถKเนQใฝVๅฟ[่ย^๊ฤa๋ฦcํศf๎สg๐ฬi๒ฮk๓ะm๕ัm๖าj๕ัb๑อW๊ล๋Nโฝ…HืตK็ฤCซŠ?ถŽIฎˆ8ผ“#4ภ—u0ลšฮ*ษ๙$อก!ะค"ำง'ีซ/ุฎ8ฒ@฿ถIโบPๅพW็ม\๊ฤa์วd๎ษg๐หi๒อj๓ฯh๓ฯe๓ฮ_๏สV้ฤ๖OโพถIธHDิฐGูด?ธ‘Lซ‡:ฝ”6ย˜i2ฦœภ-ส ๓(ฯฃ$ำง%ีช)ุญ0ฑ8ต@แนHไผN็ฟS้ยW๋ฤY์ฦX์ฦV๋ลR่รNไฟ๎J฿บดGฺถYBำญn๎:ษฃCบ‘Mฒˆ=พ–8ร™O5วž1ฬก-ะฅ๗*ำจ*ึซ,ูฎ0ฒ5ด9เท=แธ@แนBแน๛B฿ธ์BถวBดˆAืฒ@>ะญ!zf:รก=พ–>ผ”;ลš#8ศŸW5หขˆ1ฮคฌ/ัฆฤ.าจะ/ำชะ1ิฌฦ3ีฌฐ6ีญ‘8ิญi:าฌ9;ะช=รœ<ศกIทTฐŠ;ภ™ 5วž2ษ 2สข3ษฃ7ฦ  =ม–;ร™ภ๘ภ๘€?เ?€???๘๐ภ€๘๘๐เภภ?€๘๘๐๐เเภภภ€€๘๘๘๘๘๘๐๐๐๐๐๐๐๐๘๘๘๘๘๘??€€ภภเ๐๐?๘๐|€€๘๐เภ?€เเ๘€เ?๘€๐‰PNG  IHDR\rจfirIDATxฺํฝy %IU&ˆผ๏ฝZบ›nถVDDฅบ•lVป7†AtDว๕W:n(หฐถ (ศR=€ฺะ,‚ Šโศโ "3ฌ‚,BwWฝ๗๎อŒ๓๛#32Oœ8‘๗พชW๕ช๛ๅฉบ/3#"#ท๘ฮ๙ฮ‰ˆL`’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&™d’I&9ำ„๖๚๖JŽ~่าตkพ๚๙ ๆw๔>–|s+Oฎ9‹เภ˜t-1} pŸ ๐Ÿ‚ฃฏี็~่ฒป_๕yxฏฏc’INF๖“ ผ็พ฿บ๊Kˆ๘ปฝ ๗tUsุ๙็ฺƒํข๖™&0;ๆเ?รLj๗.Gv้฿๑šย$ื3นม+€]}ั7›`UแQ7๚Y ็š๔.ภ@p8r "๙nฺ้u8คส`B4 ‚๛dh่MซืใŽ7~ืetEณืื>ษ$หไฉŽฝิ๋M>๗=•ฏŸTUีZํซแ|ำ‚Ÿ๐‰เตภwŽ<ˆิา๙VภuK๊o3ทฟภhBƒฆfิ5_h่ตฬwq‡ทdฏ๏ว$“”ไฅ~ํOฟ๓œณชญว{W?ฅZknํ} ๏[‹Oฎ?wŸเศwภฏเจ‚sณ6ญnBี+ู๊Aงฺ`f ฬMำ ฉk, ื ๓ฆฆ็,๑ผ+/ปlb“œYrƒPฯป๊ขฏs๋๓ง๛j๑๘ูZsŽ๗ œ‹ภ๏–„๘nฐ๎nึฟ‚ฃYปํชN ฬz๋?ฐŽ$Š 0เๆ!4กF]ื˜oฯฑ˜‡šื+๗…๚UG.~gฝื๗l’I€๋นxฮ›๏๓-ณ๕ๆฎj~ฐช๊๕ึฺื่๋ึฺ;ni~็หปฤฺwKฉข๕'็ผRŽะืื: 0€ะบhภ! „Mณ@]ฯ1฿ฦึv!เนLAรI๖Vฎ— เyW๋ฮnซฏ๊G๚ช๖/ะปŽธ?uเ”^‚–^.ฃ๏๏ข๏฿[ ``ิ๔Š€จ‹ D6ภญจฺฺุยึผ~+P=๙{n๛ฆ๏๕œdส๕J<็อ๗๙w~ญ_-่ซ†ผ[€ขณ๖ฑ+/๖\gฑะSb๙%๐ฝ๐๙่@o€‘๘9ขฎ๋†<ัศ37\#„๕๓ํใฺุพnพีƒพๅ-c๊Bœd/ไŒWฬ ็ผํCU…๊}}__-เ||j@:ขOไ\ ๖ุญUทŸFใพ>ต๖ˆ๋่ถ‘)€v์€SiยMhฏŒ่ฬฑXlas๋6/ˆ๋๐ธๅอ_ุ๋๛=ษ’3Vน๚ข๊์ญ๙๗Vณ๚งชjqg็็pพัข ๊uwZ๋Kฎ[๚$‚O๔Fd?ฦrz๏z‹ž:KB๎~ภTRHw]ื!Bฯถทc๓ุๆงถถ#rม?ฐื๗~’#gœ๘ํฃ๗:เn„vh~าฏo฿ฦนœ›ท>>…๘-่Z฿ฅA>๒๕—`AฝชSข[ฺŸV๕@†ุGั)… F๖€O‚ฃvิกๅmทแ๓๙&Ž?~|kป๙มKพๅ-oุ๋็0ษ3F<๛ ›OrU๓cีl5ฮฯA4๏่}ˆŸ ิ< ึ๙DK๏<า=N๒:๊ž‚6Zz๊ม>(ไห๎ucz% %3ธํ@ข‹z วo67›ง~ฯทผๅ…{L&นแหž+€฿พ๒^ท@žFณ๚ ณj๓œ6’ฟQG๓ฉธpƒต–>UูhพŒ+ซ฿ƒ]Xl1ุGฌ…ภข๛/U ƒBเ”!dlร๕กesln็ใื…Ÿ~๐ทพๅ7๗๚๙Lrร–=Sฯz๋E฿๊pO:_ ๗๋‘ๆลn<๔@t™?z€รCSH๋I๖Ž๒๗i!z’หดO”B›F2]F๖.Aw>ฬMจฑตน…ญอๆบํžตWฯh’พœv๐์+๏}/^~†๗๕Cศoz็ๆ่ใl<ีตฆ-fโณk‹ž[{‡กไำภ^b๕ำ@_{sไH‹ูภ,ฤ&วhB$‰ยAWk ถทถxs“Ÿเ้‹O๗sšdศiQGŽภu๗{<พ~†ฏถ๎Knซ๋ป—ึ^฿} ž Š=ศๅ >้ืหmJึ%๐a(€ๆง7‰(=ภ#H‚NAะGPŠ gนKะ*€ํญyณนษ—^๒-ถงA่Šฃ—บ;฿๙บ๊œœ๙„zฦื-ฎY|จ/บ่อ4žแ๚%งT9zมฺแƒ‡j๛'฿บฝsญo฿ ุqฦ`ฤจ~?จFนbศ)พdN€‰Hฝ~๏๋๋%”ี"๕Oญ;``้๚~™ฎ]ƒุ{0L;fPซถ๋c››k=๔[๔}ง๊Yฝ๏%wทธh๑อ p ธฅ๗๘z฿ฤ7แฦ>LŒ5ช@ฤLิพ\กัuพฬ์ภ็๗ฉ@๎Sิะ?9<็ŽŸฆiš๔%งD<๏ช{žฝ๊์๖ำœ฿ผ%น95ธ|ไ>‰พKภง้(•Aห\โืJมล<ค2€(ญ[ภ'u“$ภปI@ +@d ]นžDe00ƒ^$ฌ``(ฑฦถ7๑™k6g๗ธ์ยw~๎dŸฯ๛^rืูM๎pณW!“<฿อ๎DŽo็]8่]œ?ัอšฎิ+ฐVกEฦิท$Ž็ฝ3ญ†์ภ์เฎแเž™>r๏ฅ@sึ๏๒1ข#แTดรI–หฎ*€#G๏~ฦฦ๖Q5’s›็:ท9๎^ดป๏ ฺ @๙็UOXภเ˜t_ฐd๕_< ๖‘ฐฺ1Y$เ#ศRะฅ%ึ_ฐ0ิวJ–„ภŒฆalmนwภญฐำiลG^๊๏๙5ว๏ไ\ พๅ๘ฝ็ณฝ pŽแ#เาeผr ็[ภทฯG€ฝบsƒ›าฆะLT€๘ๆm%ะŠ๋nY็7t๋ฬZ–‚G๗Žš[6ร7฿ฏnดัIl9ipไŠ[ทซ๊W๚ชนi฿ฐบYx้ˆพ์rKŸZ{ไ€ิ=ต่i\ ๏ฺ3}Hำˆขศ)>hŒh+/ืrW`(-m{ด›B`,tl~์ภน๐Ÿึฯโณ๏ปไเึๆโม๑๗y8๓แ ๏๏๘~iํฃซ]’ ๐"˜iถ‚‘! ณZr่B่Ž มกiช†ƒ{Oช7,ๆ๔บs๏๒๚Ol{$•“R?}๔ฯ9PัgfU8+พcฏmPศiฟฆ๘ษv{*รค5('‹ฺ#yไ3๚๚E€/น์๓GงTไ?Q ฬ6ุ[%Ad(…ฮ๚k— มM`ฬท๓t›1 ,๕ป๏U9<ฮ{~ไฌ 7jAด_ฟ,|w-$˜Mฒ.—ฒี0าŒ็h๔›JะGภG้•@ทฎ~ฺ๘AT!4๎ฏjฎ^เฏ8็ย+พ| qฑoไไภ+๏p‡ณฯ๒็}ข9ˆบiน$tึ่-0รH,u๎ฟปD1๔ƒy”+`)dูQž‰$ ^๖€<™๛ศภ(ฎƒ:ะษ4ฉฺ}เงมมxh!,่รท๙ฤ๎ร~๛‡*฿ˆwแยYเ=ฃ๒่ม๎อwัญpะ5๘๛k”Kฃล˜, ZwqลbzTย5่B !84มกYTวWฏ_ฐฟฌ ็ปงฎว—“RG^vัฦ์œ๋>7[w็‰Jaู ห?๘็9ล7gเ €ค_?๕๕ำAZw่ƒ0dRฤซิ. z๔0G๐K๋๚etาnยะ๛i_0ฒ›.nt์œฏ4PMgybTž:฿^}ฏ I'๋โzz€J@’๕—…ˆ๚"นๆะฑ.MณS-3h&Tไฆz๎ฦมรฏฆ[ฟ|kw`ฑไคcฟt๔ฎฯ9ppใiŽj€ถด#๛๚้บฤยOŸLง•”_Yzด~์ฉWtap%๐)mหV๔_ัยƒม่=ลŠ๒Ke&ะ๕a`Q9H” ‡ุใk๘lœว็kเE J฿พแธe`ล‡M๑%  ฑmดซี•IiG—€์สXค% ƒR`๊nIง‚Ch<lฬž{hถb๚ๆ?ผๆd๕~‘“V?๛;๗ผ๙แใCk๋k7&šุฐ่”€˜ฬ‰@Oํ๛4m้GY ฐงฎB–ืฅ๗ ๏ฎ>eฒ‹กพˆ้ภŸๆ๕VฝI๒Zk฿$l€‹มBฉ\€€ร๐ธŸ…s้Tt* nAเ˜Tนจh7ส`/ญ๗ื+[งt>ฃ†•—ึŸ,ภหหR,ีz ๎ฃ shZEภมกฎซ/.ู๊๊qณ็ั…/ผ๎d๗ ]NZภ/ฝ๊ฎY?ธ๑๒ูฬกesuช€T๔ิ”R%ฐิ๒งV๊Lcr^`๑ญ€dv_!ฐ—๙"๘›dIhZะS.ฟ ศบฤฑบ฿!&|ใ<:Œ™;Oเุƒ:ห'qํสฺ#๗๏ฅEทฌพฟnPฬศ2ะ๗7Vƒ_—“ @ป 1‚๐ur@วZeภวผž}nQำ/บ๑ๅtทหปัฮoˆฒ+ Žผๆ๎ฏ>t๐ภฃผg €ๆ ดฎ@คพ€’„Rv @{šถRฎBชา๔แ8่ทaถลิ๊หม;่‘๒Gp‹%ฺ–pผn‘ืo๗ฎถb˜ฑฮ„[โnๆcๆfจ:ิีำแ5Ÿูญ6C]Spไ๗๏q๊€ฯ็9@hั๔พoŒz—]AแG‹ฤT๔_ปˆํ–ป2ส๒“๒๋ฅ ๏ม฿ั}ฑdhš/ฌ~’3ธแpำ0ใo\;์๚๘ญuเ๏f-.k *Ÿ?^q(๒๒A=เญuJฑ›Y๖X†า%DYญบเlrฑ 1‰ _ฦ๕ Bรร-oข[เP/5๕์g_xม‹ฆ๙ญ์ช€Ÿ้=๎๐๐ฺUึึ€jฤx0๔ศ‘oƒ  A;๚_ค๘คส )ำีQtŠฏม—tฟฝŽ]ธO๏@.[&wV?pอCจ}๊เmฮฉึo=๓3Tร“ƒwUo๙“oสnทHฌ?ไบศณtT฿Hฐฏฤ$ “x€Nsyy3้R0@— Al {<Cจ=๕์jๆูc\๐ชOํv๛ฟพษฎ+๘ล—฿ใัฮ^นพฑแฺฉฟ €ญh„KภI;ฑz ฬศ~B๋sเ'}€ศ๏.Z๛ †‘{ฉO฿Ÿฺk ็D„๐ƒต็๘}N84๔%฿ธห๏Pr็ฬช]ซf‡fร;ืƒ฿ัZ๗2S`xeXไ4ภ ๋ึˆพ„๚ว›มCฆๅgพ“ฅb—‚บ๕™fฆ œ Bj๑ม่ั-{w ฦธc‡ลข๚สผ™่แ _๛‡งื9% Žผ์>O8ไ_ธพ>#็4๕เ DŸY •ฬ1 ™€TึE ฝัl•Pคƒsze…ศb"ะ[—†Qc0-,…5สz๗isพ็ูM6ยsgž๎ทVyT.‚๘‚‘w3yไ[†UgKสวทฌ‘K%0B๗3๐ซๅ(๐ใบ+ŸLฦข;$้?‹ฅ`’ 4<ฤ‹zํe›7๚QบๅวOฮd9e Žผ์>O฿8T๖ฺ๚ŒผC๏ ไ]`0N›๎["Jฑ `XŠม>I .*จFX๎ื_๚๚-๐นซหโ“แja 7ฟ๔ีs_ภ›๘Kืg[ฯ_Ÿนsf•Gๅฺw๘๎๓ไžฺ”๙มb๊'”Ynt —ภ๑๏-%ไ‹ฬ๗ทh>€น๗้ีHค$ฟ 2F!N‚ี–?nGเ—”@รส% pํ0ŸWžฏ_zฮ_๕ฑS‰‡3QNฉ€_|ูwุ๔œ๕5็\W๛HŽ’SoสัCw‡nA ๙BOTฦ˜‹๚ห€_๊๏ว๓Š1‹L] 3ZŽ ศฏฃ]๋ภppใ฿รpฟ‰{้ส‡ฟํŽh๛y๋[[Ÿ9Tพk๙ค8๑*๓๊๑˜เ้ฃ GพFึŸ_6ธK@K‹qซ ‘ญ“ํถG€RA(ƒ่Dฏญธvจณ/ฯ›๊ั‡๏๐ฺทํ*ฮp9ๅ Žล?ผพ฿]?Pอ|Elด่_}ๆipฐ;Aญ2งŠ`P1†ๅว‰๋ƒO฿v[ึ`ิ ,ภฝ฿/„่(?38ฃqom๋ฟุ‹?๗.ฎพืm่เ—็ฦฦ๖ทญU„สyT>R~๊†๎:8ฬ@ฎ๓๙-P๓Nภ^๔๛##ฑ๑a่€Ÿ ๔H็ล:iฐฏ0šฉ์ํภภภ๚X€ฏิ/ฦj‡ลถฏท›๕งŸuวืผเTcโL‘ำขเ/ฟ7ัซืื9ีฬ๕๓ะ[ะษe๚ถ9u7๏ Dโ&๔.AฏbYˆzปHH๛kA๗@ดTw]zH>fฤฝลฏฉแฦฝŽเํ1๗๛ย฿ฦkศป๎๙พ๚๊๕๕ญ๓ึ*ืY}‚wฎพ;Xž๖gเง% 6แ—ec<Œr[HะY๙‚฿ฐEEๅ  l0อด@ซjBฝจฐ=Ÿึก;^๐S๛กซ๐ด)8๒โ^0;ุ๑๚nSอœงลr„dฑ Œ Ÿี7t"QRฬฒฦ็ภ๏A฿.‡@_แgดส€ั๕แป97ี+C8้Ÿ๘จผึ^}ฯงฌ๊sึ7ถช™wจ| ึ฿—?ื:๐๗ 0žPnํ๏k n O=๑ํcšแ‹›4฿Rฑ[/ฆiฅฆษ^ŽBL ิL-๋—ฆ]< (%€.8 vhj๙|?แรใ.ผ๐Š๙๎#แฬ‘ำชเW_๘เs›ƒอ๏ญญใแีตSY]ด๐ฑ^Nณ•.ม0h่@ฯb๛IฺดุC๘1 9๏ึฃี_ด]{=ลณใผ˜ฝ4`Y๗Ÿ๘”ผ>>๗‘๛ใY^๓ใ˜Uh)Œ๔;๊_–โบW—'S๗H๖‹๓p1๑Z’t๕ว˜ณู ]Œ,X๚+@'–<ๆ้ฅต฿Hื v?๚` jP},@*์@฿ธ˜@10…ุ^ฬ๘ๅkqู-๏}ลๆฉฦล^ษiWภ ๚•฿๛žง๘uฺ๊z8์=มgŠˆใ๒sE 4m๐ํ๔%1ศื‚›ลเ๏#H>๎,>ƒƒย๚ฟ…ฐขpํก็็‡C๖ฎบฝ`n|๘ๅ]๓่9ช ญๅ'2,?Dด฿ฅ`*_>ฑ"CX’—ๅ“ฑฎ8F๑ฅp๖vQ!Œิ qlShฝ<0โ „\DฅะHw@Ch[๓ตทZœ๛ะj7แž(€(G^๘ˆo\เW๋อwUณ๖ฝuwเv‚๚‹ู„Pห$ฒ•Fb๕$t?‚พณ๖ฝ่ฌ>Kซฯ‡ sXฌ?๏บใ๋ฟ๓ใ฿_ฑฎๅ}W๕เ๊่กณฎ}๐๚๚Vg๙ฉ๓๙-๐ $V_๚ๅฅๅ0F๑WฆmBา˜ฏ€l‚{ษ2‹ศฎCqั‹€P=Iโฌ~L ฌ˜@0โ’ tJ`ฑ๖k›ฺป]yƒS{ชโ9๗หu‰_ ฟๆg๕ํ|ีด/ฐth?๊ธ๊g11ง…–˜}’#๘บ‰Iฯ=๘ฃี~>pเฐ4‡?ะ4žณฮ;๚ิฝyปt๏ป๒’ƒ๋๋_๘ฃณฮบ๖ปึืท1›T๐žเษ oHฆ๘Fไxา๏ง˜:*ยด฿ส/(+ุ—ัvลXk๏ฐ”ฌิ# ฎ__๗ฤ6Rหค @3ฤ Š @ ๊”ภ๖ฺ›พ๎แ๔อๅvp}”3AŽนจZฟลญ.ญ|๓ š-๎์ซs๛]“กqข"ีง8'ŸโHพ<ะGT ??v็ภkhยแMิ^ๆภ Ÿ๐=o{rฏ3‘O\}ัฦu[r๘ฌc฿ตฑฑ…ูฌAๅ!ขHฌพK/Iœ ๐หง”ไล(~ม5ศึญ๚ฤNEบ˜Aปd้ิต8”ญ)Mั~ห—ยjรŠ$ฎ€ฝv 26„+ H„๙|ŠƒใGำฟหp&หฃข0ƒ~๒'งš5'฿<ยU‹ณœฏ;Eะ๙๗NN$~}ฏ›„Dz œ•ˆ0ดžแ`ƒpเ/Cณ๑๊๋6ซฃ?qูjoœ}฿K๎:[๚ื:|ํ%ถ1›ี˜yn)B๘` วะ๐ว,๖*Œ ธ^ขRh‹\R%ภ[ A-K cq“Eข’ ก๚uHบรภ๚q„E]a{พโรw9๚dZbฎ/rฦ))G^๒„ƒ‡แฟ›f|‰๗แb๒แศีD.ด# ‰7ผgZ—Aพ„C ๓fีอ@aK ็.ฌฟyพฝqี“.{๑ัน{๘=๎๓าร‡Ž?fใภ&ึึjTพ‹ctภ๗ t‘@Yzๅft๔4Œ๔1ห”G๑9c๛d‚ล4–] ้๓CญkเCžลฒ‘, ˆt" ย|1รๆ๖๚ฯ฿่๎ฏ•kีg–œั @Ÿ๋ณ_๖๔[UavWv๕]จ ท๘6ไ๘๋ˆ๘l{rrt@„ภLฮแณ}ยฑ0มะ‡๕๗~๊ร>rไศ‘ี฿ฟ๑พฟผq๐ุฯ8ธ‰ฺูณ*๔ิ_๛NZu‹H๑้$€ชWึY~=5wฬ’k€ฏขไฒd +H{๚m†ู˜๔ H ะญ7Aฬเn๎€ว๖ขโ๙ภœs๗Wฟ๚Dฯ™"ื'` 3ำๅ—า็ๆg-ฺก†ฐ6ƒoฺjฎ›}๕๓?tlง฿า[&๛ฆ‹ใม๕c/=pp“ึืจf•๐*โ~เ’ „๎Aภnำ\.Kำ๕๔วVว2)ษ๒—€ฏ๓ีถ9>`ง๔?{๒#.€Rะ๔_(9H(h`๔ 4„P{lฯื6ฏูžoํ_๏f:rฝWง[๖๎{๏๓w8t|c}}ัBKฉ,š>?IPgJ#zาๅส๋dฌ€ะ•,EP ๚Y“€๔…ศm1˜ PใJ, .u€ก2%ำ$@๗š1ญ๙๚ฤ์7ฝหk>{ฺโ.‰;๙*๖|่M_ญีG76ถ6fU|ว๐ิ_Z}ฤฆK+ชYY_๖;a๚ไฑ.^Hย6๋<—–“ค๒™๒๒ฌŽฃL’NbฟUฎ!ข1ืขไaˆืฤร๛6๎ไ}ƒตj~‹ตะๅ]บvba๏eR+สัฃ—๚:4x`c๛ีฌAUu”ฟ~?Vก๛7t๗E‘ึข๎ Q™^ษ0Wศ+ ส}ะสE.ูaุPเ—๊^ใ๕คb๘Eภ๗ฟธฑ2ื*ชชฑ^m฿็+ว๘7wแ!๎‰L `Eนํ๚ฟ์ฦ๚๖ืf5fUƒสuŸู& A?ˆ>Qด_Šึ|ง:‚G*‘€NพฤSฒฌ๓2ฅ ึ7–ฎŽฃฟ*d]Crญึ=ัมFฅ”ญนษi ฝOณjตj๑c_y๏eแS:#dR+ศ่โ{ฎญฯammชjฤ็ถปOกcx3Qk0Œฦ•ศ˜"่ฌธœ,\RŠ |I^rmf „%E`นฌN•%ฃฌfโ—ค๙}5ฌ:าๅ~ฌถใบŽีdl@ฐ฿~{ญZw‹฿๙ท๗<๊p=“I,‘ฯีฝธj๒ตู|6ซ๘ฮ๏๏?ล…N€า&—๐H-1ชZฑ”มŠt?2,ฟฆฯ†๕๏~หJ—ฌปaีณธม2ฺฏ|์{šมษงร’mqํ-3ฉ4หUณ”@˜๙๙นฎZผ’ฏพจฺq#C™ภ๙า—ช#kk‹o๕~?๗ใค่ำภŠ‘๖˜–ูษ+ห’อ๗่ษ<+HV๒มม ŒภVก4๚โต –Š [Hๅ&๏•ซ๎—ึ B๏$J ŽJm0๓5fn~Ÿ/ธ้ฯž่ำ ™ภˆํ.บำZU๘ฺฌFๅC๗๕]t]ิ+ถM _ส•Œ`฿Pฯ#๕งยถณ"้ฃฑiฑญIAhเ[้…๓1ห@l/ปŸฅx€ม,คŸ_vZp ็ชj™Ÿศปญp๗V&P#GเฌyมZต˜Uพwฉ๕บ๛(”าL6็ปุจ่Fo๚d”‡Z,G ภฒh+ศซธ%€\๕๑ค็,๏ eภJ1ฌ Qฯ@3ท๕ถ๊p ึชลฌš๑ห>q๕X๑i๎ฉL   นห~`ญš฿ป๊f%zฯ=๐„ๅ—ิ฿คEไ—,๑˜Œะ๛l[ƒUŠdพช ืๅœ]ืX็a\oF ื+ำฌ{YRฝ๒ะh|โp#P๙k~~๛s6Ž<ฎ2)C๚฿}hอืฟ2›uเwก ๚น๐ฑห/๚Azส๊ซฦ>Šsํง‹tซgŸืฆ๕ี ,ะr f3ข/”Aฑ๗`์’ฺ›๙๚/๏ —า€ฅ.Rroิิ๊l#>3ส:๔ฑธG *ฟ€w๓Ÿ๚๚พoร.“0ไf7š?}ญZฒ๒mิ฿eิ?eษ<ะWฝ%pPถz]V@ฒb๕ึิ,f]ฮ! ฦํงฮ5๙Yืe5๘y(3,W ฤšbPZ{u›{Fะ?žศ•o0ซๆณ/^ยG/๕;}ฺงS& ไGxำตjŒพฟ฿ล>$๚๛ฃ…/ฌgJ`ฬาŽh.ค้ฏAซ%ศ๔13%˜เ_น6}ผั>{‘ง{>FPบ๊ฝœm่>งUฑ‘&?อึ+ŠศBื+ฐ}ฯ/|mxฮ`™€’ูฦฑŸZซ๊sZ๋฿Fw=กŸิๆะั่—ภฅeoื‘๛(‘ึฅ(๒ณ"โ‰๕”ภW็ฝ*xKŠ`T‰๗EGฅ `ุบN}"_งlI้าผง๚ธ…{.oeฏSW`Vีฯืซ/=gจL @ศ๘ปพึอ๊'WUdl2€ไลV @&*ค/ณ€Rdฤ[๓ำข ๊๊Aๆr*mบฬn8ฦR7‚์ฒŠึ)ำกผœ๙7ฉพ˜:ผ“้๗ซk]tFn=ฮP™€5ฟŒ๕ช>่]G‰“9๒#ฅค้>ูnฌy๖ฆหะIฉ‡j๔kฺ็‹rE&Rš4]๗ํซs๋ํD}K@ฯ๚<4ต/Xฌ ศฌ3‹6าฒ}ุุW–aปn-ZgGw {;•ฃ€ส/Pน๙}—ki™@'s๔ข๓ฏŸ0PN|8์ทลนeล#ิ้ซˆU^SfŒt]‘ ิพ’ ˆbภจ?‹h๐#ญ'Sl๒4S1,z‘๕irA@ใžษใ—โู}๎ข‚‰'=่ใฒs\€w *7w~V69rฦแํŒ;กฝ’C‡๘ฉkU}PŽ๕๗Nt!eƒๅWด~Œโ—˜@ ๘Y=0"ใ–Ooไ๋Ys™eVว*YtํR$–ศ๘1ปสOส˜"‹๖KOjGย๐–!%๒› }7โฐtฤจฺaย฿ฏรฃq†ษค|์ชžํ|๓ฤvฤ฿`#ถใxุ๏Ÿ๚@nร ภ2๐+Jl2ˆBูา˜๗Rฐ-–ษh๚’%ฤนikž—T9ร]ษหI”aญใ็ภกสeเg•CypูeฐžG ๑X‡มAžT~ŽชZ๒g๊า+^๑i‘Iุฦๆใf~qฎ๏h›#tรkฝาzXิ ่i๐หuำ๗ทค`I-ขฏ[าw eดฏฎำM6 สkฐ'#๎ฬ‰2!‰•_f๙W‰ฌD-Dบrˆxpึ?g์{ภG/๕ี?:๘f๛‰‘ƒ๏ฏจึ ฿€3๖Œฏ'ม0ˆ™ฒสพb)หgŠHญดข๖ฺงg ํ˜JH—<ฒ=คrซ\Œˆัžี`ศช E>H%„X ยฟ~๊/พิzJe฿+€๘าƒชชฆึ๚o๙‰V`N] w]๚{‹XรoQX7€Ž”—‡$]m›๓่ญtฐSฐส่sฒสษใI /9ล—นq[,wa'ภทvxd @ฑGภ5๐~๛ฦย๖ูaํงL๖ฝจช๚‰ั๚วnฟแฝ~@n—ฝ ฃ๔[ฅ ฌ,คฮฺŸG ^้—›@Y7ๅ ๎๗CTPCี5ฦ%€x}ํ฿๋‹s[ๆ่<๋ ๆษeข"p ื+ไๆO๛๘}sœฒฏภt[y฿< N๕~T_“๊Kw`ลwฏCพžน๚G9ภeฤ?ณพZ ฆŸ>ฆ 2ฟด/าz๛ ”[[Y‰|ต7ไบ>ฆ*฿งมฤ]0Ž)YŒ< ๚คkฐMงN xื`ๆ็‡ฬถŸq"mvทe_+€ ฿pๅk/g๛ S~ ไจ๏^tตƒ๔๑-เ๊ทเJฅŒ+—z์…}บXjท J(Yjฒw‘ณg•….ฯฦ0]•dn@ ๐ฅ๓แ\ฬx_-ท€ํ็‘}ฐตUฮ , r๓'~โM{?Dx฿*fPๅยyืคƒ~บทฤ)ฟYเOำ๗าtบ_ขHทณ—_Œฐ์ฆXิสzkŠฏ(3€‘ง@šื๐ำM`บ>s}ไฬ๋‹็ย…ใว<ษ˜Fส๊u ๊\ื`ๆๆ‡žU๖X๖ญ๘๐U฿รป๚›๛ศฟ๖ป†ฉพŠ๎“ถ๎า๐HŠ h—@wŸeสFYไmM3†ฬ-ˆee๔^ิSŠซŽWdŠhŸb ๚ห>:ะ˜mŽiญ'็*™€A๗-wฦ๙A์ณฒDEะฑbxjเ] ‡๚‰Ÿฝ๚ั7ูImป-๛Vฌ๙pY%F๕ณdฟ๖omๅ๛๏E~)ยฏ๋เzŠ ขPถล , ฏJ๓ม#e ขPฆบฌ{ฤˆ…d>ไบ<F(ฤฌธ@ฦด,a^ิํœ{(๛R0ƒˆย#ผk2฿?ฟคYฟฟค… พf หพ€ฃ฿‹ˆผ๘3ม˜๕N๖/๘าะuTฝไ๓g:@ไผ D‹F[็ิ%ƒqM%—!QJYŒ-ใฑ—Iฏ#†ฒิ+•ซแช๚)ป๊ฮ^^ูฉ‘}ฉ>๖๏ผ“Ÿ5฿เปi›ว<๕?iเ'ผKำhฬ+4‚b/€ะด]ฦ:aฃํQ๚ธิี•Q€iตูฏฮวpbf/ิ1-šฏืล2ปVฅX ห•ุ๋ชG uบšŸทพ~ 'šON๖ฅXใๆ’Š‚šํ' l๙W๎YA๋ำXภญ7โ"x)(จ_›ค๛มPบ}.s —าŠvYšส๎ห:ญrP๛X๙โฌxC‰ๆ'ึฟ“๕Qงุก๏ๅ๊h•ัฌ\5฿<๕cW=pdฺ๔‰สพTDแqไ_:๊}฿:ฎ฿๘FF•oŽ๙_ี ค ฐ#:p{zmvัยฟ’ีต 9Pณฎ#uŽ๙๘–ข๊ืƒ:7qํลx†F9ซ}ตาBY๔ฏ‘๊&œu=•พๅฺฺมG-oนป/๛N|nๅšปนŒพ0่GRอ–ฝ๏`ํไเืฝ‘mซ๋p๔งY)ภล<‚ }ถบ๘Jึ< เ้ฒŠ)ซN๑03eฃnk=žฑW4๘ึurI…๔nฝ›,Y@๋ ,ฐF๓Ÿ`^Y}ูw `ญฺพฏ๗Mี๚a๒?ย๗—V_ำzณ๏฿Š ฌ?ไe–สโ๋ž}uฺ=Lถ  ฝNRึ7s $่ ๅฌŸิ\ผคœf'(œT96ฎ))#า สe๗ฤบนโY๔ฟdƒะ2๊b/๎๘้ท]๚;jฬป ๛P„๏๐๒rฦ_๖š/ฃ@ฒ—่_iŽ@ท—„แdส`‰ŸŸนโื็VฒHeๆ฿๓’ด’ป ้ปฅ บ•”EษMะ`ืพฤ๊+6#๏2YV&ฦbAp ๐~ดถ็“•}งอ}๚O๊u_ ่Q;$พ}ฺ›SWŒ๚วผฑ/ๆHะ&ƒxฌ กv 6ะF(‚>ฏ๛“Xj ๕Ÿ%/Y}ฐBีฃkะ๗’"ษ\ ุ๕[?‰่ าฅ(0"ถ๛็l คG€(~eบig V‹฿ท฿mOขy๏X๖•๘ฤห.ฺ๐>|›#ฮ~WœใoŒ๊ฃBzั๚—xf*าฏฉ๙ุป๔ณwˆ:-I@ำ%dTX)จโถ—y(”Yส–”้ฯ[ํำ_Ÿf๒uโขฒuสำ“Jฒ eˆ๏˜ˆ/ i0ฃน[ฃํ'ฏฐ๓ฎษพR๕ืฏ_เ)p}&R ๓ฅEoง"๊—†๗–ฌศ—uฌฎ@หะ“‚,j:4๖)Zพ๎ฯส?ึQดiƒก$–)ไ๕ย8ิตk%}ขœXค@'Q—๎YQห’$DPณ€vpรโ1z๕ฅ‡Oชก๏@๖•๐\฿YำRฺož W€J>’฿/๕ ุม>ฒeึเ5ล5hqF๗ฑ"€เ๛ci๗bฤสk…Q ฏหt ”ษ๎๛๗B฿Gฝั?›ฑ฿ๅ๕ม@กไๆ7ฺฌ๋ำ๖๒ะ}ฅ*.H>๎‘ผํ'H€W์ุ!่yงŠ ๋ี[(๘}Lย>๐–ข่ˆ ๛•ฌบโำบN่ภ“uๅฯg๗E<ƒ•CY๊‚ƒ1เ\ O๕Oธ‘๏P๖•p๛ว`๙[–ฏ-ปฆ๖:&P๒๕OPdoึ‘iโK<–าHกf3@ฺ่‹T=–S4฿ ๖i๊]R…๔ธ๓าXสu& ถŽ%ฎู>็ฅbฆจลฝ–๋Zโ†๚;ะนž8š฿ๅพ๙w?๖ฝcฉNวAฮ!n“N๘‘พ?๊S?s่/P|ุฝ๐HูโJ”=๎Oฑ”วO๖9'็$ซˆ"สืฉฉ๓้๗ีดWm—XqŸวF!kข2โณ๛ถJšชฬ€ณฑŸ๗œ็˜ด๗7 v“„*W?ภ{qŠe฿0€]๕ภuษึŸ,PŸจ•วjiลhy2X" –ๅIlฤ4k]ฒฑฬรโBฅs)=ำ๒๓GฌRืyบผ ‰+ŽๅฎฬคผL๛Juม pwผซฟ๏soกC8ลฒo€_฿>฿ึ$๐\‰oI๓K๏D!ฝ๔I๐าWqJn€I5ตซ ƒ–RเดฝY Zบ?ฆฉ*S3Vช่-‹/d7ฤชS_็ื฿gY๎Bแ$๗‰หykะๅ4๊—-๘# 0œซQนล9ว๙ุ#–ท์““ฃศ]โ“xำฏฐ™๓ธ์Œ@Y๓%ฬกocŠคหOrม ๒}%xdใ6™Aษ฿๑ั—)…’Eืภีื’•มpฎ2ะหs“e๚็ซสsiฉŽํ ‹„V @v ึ๐˜?งX๖˜Utำถป‡>l?พd๑w่ต๒๊?ำ@ภT8™E’ฐEฅŸkไ–1ƒ•ป ๅ9ภฮ+–—ว@พ”Q&q|]Fก|%ชฏ๗-ิY@dhg zW_๔๚จ[โสพQกแ๓ฟ$ฐฏc'๚Cพฎ?ส9 ๎[P์.Lฌฝ5ฉฅ๑“’€ภ{—งƒ^f/€่๋1ฌผ ๎% /น @ิญฏืขšQˆzฒ^ ค็.๋S‡/*‚สŒnPŽxท๐.?ฅำ„๗pฮMพุำธ%ภ–•b€;๛๖Œตk ถ—ัD JXŒFdสbป์ฌฎU(2%˜สภขฆฒะส้๙#ฏfX็t]^ใ่=ฯ+mŸขˆvd งๆ๛Wx',๛H๘C, ฝAG}}`วŠมฒฺผฌ>ไ๛h๋žํ ,9X๒๑M+ฅ่ซ ๚1Kmน;ืr L๚R 1{aUฤธ~}ฝ๚พภ8ไจ+ ุƒYˆ๚<ฉˆธ&์฿๖ั+๙ญ8Eฒo ยJ%0Vถด,ิYทUถ“ค‘,๒๔Eัๅต5‡ุึด]บ”o*…eิ_—U็RŠCdฝ๐2…ผย>:ฦ•—(อไณ"จฬaฝwMฃ@hเPำฬ7—โษพQD่น&€ฐ-}†๊+ถW˜ี–Šภส+”ืวะ3น TL๓ ช›๙ฝ2/ฎ€อึ‘ฆg:N๋อทช;˜Š@[`SQศ๓0ฌvๆ(Eกื“ฅR”cB‚`xeัโ‘8Eฒo3ญ ฐ Eวพุ›h๒%.€์โ๓ื?0ช ’2(ื/A_ผ10ฃA9h€ำ/นล8€*ว ค+E๔นศ"๔r“ิพฦฝЉ’็žล4+P็)Ÿb๗!'ไiqว_๙ศoฦ)}ฃ"ฌA Œw‘ษ๕R0ฏ`ฉ—M๛ๅล‘ล €ขbศพ๔+๖ษ^ฎู˜๏šะ™ึญdŒ 6๙_6้'ภฟู_ฏ•ฬ*ฎืฅฏลR ซฦธฐ]ga฿ไ8้ษ๔ฎ@œ D5อโœู7 €‰Bwmใ~$บn•หFเ้2า.Y F•ฑ_ษ๚›Š นxฃมษศC+_[๚b,@g)k€๊˜2๕้จ<ินขp๎Eล`งฯJ%)[8$‹ฬ%๔ฟšbT`@ <๊‡ฌTมe฿(o๗!@Ž—1ภa„Žหt•oอี€ดาญบวX„>๕0๔L tŒBšฅJI^ไะ–ธฆ Œ‚:c(\ืK0dึบค๘ไL…`jแdั?–ฑ€ ๆ:z้yุeู? €iซ‡Gาๆ @ต"๋M๋บคwก๘+ลPจ'žงq,.คY็ld‹4)ฐมd^‰ขK๗ศมhนY๐ฎไ”‡ลŠJฦ๓Iำ็ืง—”PุO‘‘ จh1[X์๚[ƒ๗€รฑ$˜อฟฒb—\Rถฤ$€2KbX๙d T:าฒ๚ฺคภ’rR"ม VNห˜y%&`ํำUถิ ๋œ„ฑญ๗ี ห*#ŠสกtŒำPT&†Bˆ=@"บŒ๚?์ !ญ$๛็}ฎw#๚‡ง)4,Šะฅ—ภ^r ;พ<ซบฬBโผ๕zท/์Zช+n“Vz]Riืป โa›ฤน‘ษsPy่๎ต‰๕d*๓*Š@[เBู1?=S Vฝj]bา๛˜ฯexm €ศล—…{fัhˆsGฒo‘P&]ห—PXๅ^ึu8VKาฌ}ี๑ญ†š็Šฒ•2Y€ถ๐†{`Rxไy๚–ซ _Onฦ nBZใุู๕+ๅTบr#๛Hท!ษv ‘ื๗ ัโ๋๎Oฒซฏ ฿7 ฐ๛W›CดZมp/‰๚gหๅN๋5๒3v ญฟ8ถ๚˜‹S:-ฒ๊ึ, ™Tข4ล=ท‚Z๓„Mpz‰ษ}๚nZmฃนญ-ผฯสณ”‰ฎžUžvด2PทpB฿%ธ๘~>Š]’}รฆ/0;1@IขฑI=ฤ‘ž‚"pKฝบว`I—bชžx^ู[‚€ผ[PœS฿๘eล ฺVnfญ‘ฎgyl์งŽ5๊›‹}‹= ฦว ศzก๒Œ{bตu๚ฅแ/”โ่ภ‡Dอฐ‹ฒo€็ๆ3œ€E5hiฌๅCYูืK ภ4k๚Vฃผl`ฅ๗ื—\™OzGJ'˜๋ +ˆ>?ฉz8=y{Hd4าฦ…ํ1+n œQฎ#3ุRแ(…6zš–๛!žiw‰ข„pŸx7ฑ ฒoภMq‹mqlx๒›Oูส ๆํcc}๔1}7~(,ญ๘‚๎ัฐบ0ปยI›7š›ถR๋.-+ \JF ภh๙์š ่eฌ'9–fฦOำ|mัณ๋U๛eฑ นฮให‚PW†ธฆ๘ถ O๕ืŸื=โุ%ู7 €.~gอLŸฮุI‚%bZSต]๊ฃ_ฅ๛pl&฿*๏`ฃ|rญฅ.K๋š4ฐฌ{'- ๐Jะ่บy‰๊k“j ญ‡;6€#ว# รJJศLิ pTSM๕=ฐKฒo@wนg+(eT ”"์€ l  LZฒฟ>žผ•—]—ฑ๋:0œำชฟ>qฺ๐กหP!_Qe –เ†ฅทXรุ ฃ"ˆG๖MXY็{{วใ”‚ษZฦ๐ยPB@@ฝk฿ ุW €เ> ฆ.ฌ‚”ฏ$ฒ!u๊๔Pฅ฿,t๑ษบ-–ณ๔ำa+t j:.’หม1ƒZ›€%& *6ป๗DYบ>}๎–› มnิ—)=ฑญำ“ฅบ๊สชฌ ‰ˆ›ปb—d฿€ู#ร 74Cแภ๐กKุO€%+ฒ”‹ซซe=0@฿ๅgืฎใั=(ฑตZƒ†โpX\—$V~1Y\–fญหบูภขPœ%>zŠPล%`( g…˜wuPงคcท !ฤwญA๛Šย?p๑-ปP Cฃา>]QJVบi\*W:WE๛ว"ฌiนฎงฟฦ%LฦxำS}‹fหc/กอiPวีt ็%ฅศ๔น~ึ๕iฆ“ฌหฅผ๏F{ฒ˜U–ษ๐จo๖แ+.ฝ9vA๖ บ๚&฿0joNˆ้•x`P้ษฬB ๙}yฮทฌfัODข•ื'ฏฯชYืIYRถ!Cฦvฯโ}U‰ึฝะ๕hKŸ\6ฑq<pฃมฉPืa5(”|ฉ\ถ๊ ด\วˆ-x๛v>‡“”}ลฮฝ๘พาภ}2า…1‚Šฒ˜V$สX0ฐฤฦข๛+ฤฒใวbไวdภŽ‹pžg]o†ƒ1˜lJV_3จ๚xษoIYMแว†+[็ี฿Uถh5ฯ’—d64 pจ/Xฺ.W}ลภ1ฝ—™พ)˜%k.Y€lx$สg์@Šฎ[๚๗œฆiซ<บ฿ึ`Y} ๐:นT็-;ฎ.*ฌ~โkf ๊ฮ็—RฒไET๑’_ก^RŽญ๓`ฉุifสขษ ฦฤฑ๙ต,€นนอฒงถŠ์+ะ^ฑ/ณ˜wฏ8tKำ ภ }€ํ๏ำาO4๒oฑ `y<`Œ `จƒญ%สุ~ฝ…/ห๏ถ“1ฌtU้o”hf ฏE[๙B)ต๓ผํำLvี๗จ;f๛ๅ ฐ๏@ร๔WฬิดŠ"-ฝซ`}ณไRฐฎ+l}าด๘…บF>fญผธ๋.\!N1ฆ%X zL$ฑnm6๔WnHf`๑e6ถ:–Yy]#/วศกˆ^ืเ๛f: ึaป!|๒‡ณ\๖๘๒โฺ}sทqจ๖– uƒŒ–ฝ6ž‹Aอw–blๅR:ห@<ฝ‹9"ฑp์ค˜:Vi™[๗ื้}k,๓ ้*?ฑลŒ๓ศ‰z^E@ƒ฿>ญdQ|ฦŠ-1๘๋wฃ+p฿นทพ๘[!ธลrํJ~ŒB@๙๖S!o…มAKฟ$๗)ฅ[๎ƒ๊:,ญ@ >Žบ7 ]Fก ็๗:s bžEK.0๎:๊ (ิ)ฯU า:^ผฝด๐ฆษ™ฤ:EZขcธภ‹ณ?|ลฅ‡p’ฒ๏p๐๏ ์„๕PำJ““ฅIe+iP/wXR.žKฒDn}Kภeำ„อ๔s0ถ6m,0๊็ด๒`ภ๖่H?.”ีuˆuื["#Fู>™6โLฬ๊(’;พ8~3œค์Oz/ปjดzNะ•ŠŸexYm—๛d๙ฺ‚ถb@9ญ๔•แฌ^qฎ๚บFส๚™๗ €ฦพฆb(-วภ?pใFŒ“”}ื ุ Wo\5žŸ=ิพ+่Xึ-%ฉnท]ยB฿Tื\ึลc๔ี—ฦX็ฑRนุฐWํ”ขป๚Vu ๗'ฉ‘)Gd&ฬ‹UYkป qx$๚zฟย9่kแ‘ua`oB‘‰ถ8#‰) †ฆ^€•C๗}ฟ4์ฆศ ศีฒคง1Y‚kค2จc]f•nพe๕Kย(๘UC#,ข$&…g‡\ุวชฃ@แวส๗u๓’บŒฦUY@ทS๒všืน g8Iูฟ ณ{w/r!} ๑๎าคฉษฆฌ(i…`(—d0็ๅ’mคe๛]ฌ|yeสษ2ษห˜€1`๘ดณฌไ~๓ศrŒศดฑผnนœWบ&{ัnƒฑ.‹ฒ–[ฤบ๗X๖yฤ'mภ๗-วี๋;Nๆิ'ํm v‘’ด›1?*O–Y%€Wศ—้ึืLw้K™ฎNัต๎หาูศ+ฑ€‘ไ…Ž–KŒีฉo\ร*ึ>Q€๐_8iูื `ใ;^๑ภณ๗# ฒV๚ฤ“AJฑ,ธ”Uฉบ5xgี€RBู•ล|Vy™b3‹}G้;0n‚ >}›อ๑HŸ—นm) ๙lq%p"ื4l็€gใ'š\ศหFฝB3•}ญ  ๎5ร  ฃLj|)[ฬ๖‘+€t•1ฃ]‚+tZ็4๚=’BะวนEฦaAญ•2สZn*ฌฺ2ห”Bf‹Jก๛6ำ๑&NR๖ฝ@ุ8ฺ฿d]บoฆh‰TC-)Tซ(น=ึื”ญฒ.IK‘X้(ƒ~U‹/ฏ?ฬNY(ฏVd#วฦฒs‘ํDค…qๅa๔จ `คw๛„ฎรIสพW๏๗šฯ0;”ฺ]*ค@ŠEUฌo?ZV™ฦ^Xrฉ~u๎ูuสa๓YชNฤUฟ1 ˜ย(3เ‘6ฐCิฅi}ท~ŸRL U „€š58Iู๗ šฺs็$_ ฒX@lT๑I ูษJb)–u™ญ ฐ@ฏ๓—คeŽง\ฐ—˜>*๕+2CN๐ฅ4#ฅcPบV=ฃฑo๘ฑคึ> UaHฑYฮ›ฏเ$eRฎ ี๋ฯพฺa4NQยX,y‡Gณึึบภ“yห€‘€ิฃ๋& ลกบข‚•—+R๛๖ฌฌ84๊‰t^๛2š฿ฏ๓๐^)t a(ยยำwุเ2™€›]|ลuuใŽ"8qšฐ่’Š >M y๒ซ่Pz9๎'ซ(Uzิผl]œปพะ0ฏL๛—( }=Ku+ฝrคข๒ล๓วๅ๓€ฒˆ>๔zขH0๊FฤธŸ]กตสค:išต฿ ม.@์c5Gy) IYKŠ฿%XฆDนฅ=$}Oภ:ื‘uญฌฯŽ%๎€พ':๖ ฮg%WภธวK้น<}ถห%/7ลศ>๙q-Y฿T?–‹iกง„่บฟz๛ต8I™@'/zํ๛๙คcH<\๙ไโ^!mˆ’)X.AฒU‚+ฒ‚ั7q=:๘W๊๊ณzL%ไ€–๚XЁŒ "*ึ‰ฤ?Gฒ์ึ•ฅTะฏตŒ2-ฺะEC?๐Mpจƒปj7๘ค–ศ์ฺcฟ‚—ž$aKํt๋ฃภ7$กูQJ`ถ๒Œ2Lๅ}K.„•.A•œ›ก ’r” tฐ@ซ˜Eฦ Dฺ( วสบ{นN€€–2K#๖I0H‹/}Cบถ๘B4-Xิห[อw-L `ฉะ%WGS= ์นzrˆ์ ศ Aฌหฤ‚ฮม2+฿--?},ขฟJLกTgฦฤu˜๑ฉ ”‹PฺO*‚lปฤฦ,Eะญหy;่iพ๒็SŸRšŸEEฤzฬ B1ฤเ_:๐'UMh้/;rล|7๗คV/lW/แเลn<:ื!!ZYษ๚๖XV]ฆฏ่gห่•๋1 vค็ณ4Eฐ\†e~้}์ฦ @ บoญ๋‘}2จcฺ๒wวf๔ ๅ๒๎>'ภ๏ะtฟEใP/f/?ํzR;๘ะหนฎ>j*ฤJ)E€˜o™Spdbฮ๒ั—v—‰็Tุ?SLe ๋ะ์@w#jเg๋@ฆ@ฬ?ฐ‘?CฉคSx%C์Q$@™ฎฦ(เงพฟK\&D๊Om๐ฏฎaฟ๒ึ๗žŠ6=)€ํ๒Ep๎็Z7 *ู#ี— ˆ ืหn=YสF(๘pFศญ๙ซฟ4๊/ม^ ๑ธK๑ ถธ?Y|@ไื%#@มส+6!ต>ชo๕หˆฟHb”ฆq5จฏ๚pg๑.ภ–๚ืGอt๙˜‰8™ภลว+_ฯก๚ซtp”ตaไ๎@Wล d,AฏฏิฺทๅำCCw&๕ heP๒๕ีy,‹เยrV`†ษ8š๎#U ชปฯV ศ•€๖ม>nญ<ณห(ดง๚๖เo‹ฺ_๛•ูแWžช๖<)€ ˜SŽ๓ฑบA"ต๚&่๘Jษ'เทƒ฿JS้™ี/ิc๖ ่๔1& ฎ!cศ•€4™ฟR@gๅ9ต๒}šE๏นuืJ eMŠ/ำ\‹๛ƒ฿฿4ฎ๋๗ฏx์‘?๚สฉjฯ“8ก๛_†P]1€_|Q(›Tขปีv|Iฝ“#ซ ะฤvฒฎK#้"ew@+•o๙๎ภˆ ใžh`ๅiRbณK@/๛๘u7่ำmgษL[6ฦ_ฆ ั฿Fƒร|Q5‹เžw*๒4๘ลฑ๛in%D8ะถ2\ท\ๆญ๚]zแRAŒิG…๔Xy้|ฌt™&ONจTV2–‘(9Jห2‰2๊ฝR”๛ะฐฬ๊Q—:Yถ']ํ1ธซฏีป6xX็ถ์0…—บS`0†ดaภ็ฐ๏0WคณJื/„†E/@฿๗๏ฑO.๛ท|งP&p‚Bฟ“๘gA~G ๙ž€๊ฬz,wภ สuหท‡Xํ•ฉฒาบjฟ฿๊๎ณhฦTดฅง๚PQ%ฟˆอD฿๊๎K~d–? ˜Y๛|=กVšะวฺ`hฺ฿ลยshฏŸ๊v<)€“/,š_Gใ?™Š ๓GY)‘Y๚PE”„2อฑ‘^R YะŠๅ๙vRNฅeqคKถ–(€Jืeา™|้Œ= n้@ะxฑoฟ…บ่?็J F๚“A>ะ/๚[%Pื๓Pฝ๕{Ÿuี:ีmxR'!็?เวฬ~่,Fจmตฐ%ฅ^Rฤrป;ฒž œXRnŒศ๓I˜ [ช๓2๗ตา;ภW;D   ช.?sฤ_๑#:kฎ•AแืF๚}o๙Cg๕›ฆ-sใ้tดa:๙*& ๖ธ7Rี<ž8€c€Hผƒิฏหˆฏ๔}ต7YูฌหญX”Lƒี2I7สX๙ฐ๊ถ๒ล9%lGŸฏq,ˆsKส้t•/าธKk๖6=๚๋C:ภบS’i<ฤ TZ‰/นฮฒ/qฬIAFิณว #†}๔}Gh*4uีŽ๚kๅxฮvๅณ_ซศคvM้น{[%G สB€ภ„5Œ้ [่เZ<ถ๒ท”)$>tฉWvถ๖Lเ๗็dีบฌoe๐ำ่v:บoz,Œ๑kภร๚%Qง{ไภ—ณ๛šุฯ๏‡>P๕พผฉพxlqึw:[ํคvI่โwึTฏ'4~ป๏์_6Bใ^โƒkI”฿R ด}บุ_ๆeรƒ๕9(+nu?f๋โ๔zvM@(nณTrŒฆRk.‡ŽG๙ีP฿%/๔เFŽ๚ฃ$า฿๖๗{4อEป™วผเพt:ํคvQ่ปž๗ี3๓ษB๊์ฦฏ)จ+0ยฌ๎บ„-ฆ%.) mๅกภญ•Žiํm]‰%-aู„ž๎†ˆ>D>๚ั)ๅGJ๓E”?eฮด๘#ˆร|]2ธ‡%๐C…&T=จ[๋ํ_zฺ์้>เ ]๘๊#๘๓พ.tฝˆปU.ัะCP์l+ิ:[JD๘$ห€X/ฒ‘<ฝn•….Sจ:‡ฒู>2/๎ฃ๔1โŸE1D๚ป๚tC-“z๕F๖ๅ๛่_E›>๐็ะ0 cป่ข๑ุjๆ"๘ป|๏๓_งปฝN เฟ)ท…ใ๗ร‡รƒ่q ๘ๅ ๊ดC฿ @ชG@๔`ฐ”Y…ไQ [Šฃ ŠภQ>(์—(…BZ_พํๆใn{5เ๋.ฝ.ก;NศAบ โลHถ฿)๐ใx†#๘=๊ฦaxl๗ŒG:W ค์FธHืณ!บ้ม’ 4ีฦญฟ‘๎?~ด|‘JืiP๕Žฑำ=ฌ๖๐]พaเOดิm๑eƒ| ๖Sึพ/ฃ)ฟXฦo๐0ะ'‰Vฟeqะรขฉฐ๘/m-๘.—]ŠO๏U;"!€ูนACw่›เฉ Dm‹ัฎ~ฆ‹วI ว,•fZUใ`œฬ๚3L‘qฬ_!_ฒส@๏_$ืป:jปu ท๚ฑž~_G้c้ Ÿtึž9๘G‚*›Ž๘ฃ๗J e 3๐ญจ=ฯนze—žฟิส&ูEแท—ปข ฿€ m ภq `eเEL0€ฑ”4<Œล`X_‘f2‚Bฐณ|X๙+)ฒ`Oืsหฮ๘ฺสซ2r$Ÿbร๔^e๕…Bˆ F๛ล๙ƒฯ฿ฐ๏}yp˜7Y{ั๏=cฏ็8ลB๘ญ๗ีSัx T" ฿)(ว ฌ๒+QnˆtีS ๓bพ•ž_…V:7๋˜cKXy๙/้ึ๋gํ!‰Hฟp\ค/ํฐ&๖ฐ๎ๆ‹>ะวชซOv ๖ณšิโ‡P๕ฟ:TX4ณ?๛i๎ปำ';ษ)~๋3^Œช๙‘6ภู่@d ะ,(็€tษธ},@Y์,ุ€ล:ฬ๔16 ื-6Zp0+ซ—ษบ ึ?y‘G<|’>ะ๒ณŒ๚ทJ ๘๗~ฟog๚ฑร"8l7ใhpฯK.ฟ‹{&)p๚คูz่เํ€ๆพp่฿ M~ท.>H๙๙%acฝไ?ฏdฉ๘@i?๋\ฌ๒บnŒ”)ฌs7๑&ฦ ธ๓๓eœ!–aSo๘ัo๋ผ‹B< Qรฌ>Žภึpท:ภํv<๛๊f3{ุe—?Œ?01€ำ*ึŸผจz|๓ํ๘€ศBส› $j[?ฎ[เ_ๆ๛›๙'˜ใ8'ภาจ>„o€OูAอGž6๘8}ยHฤ็ภ:ฐวˆ๛๓จƒรv๐๕๖y๘๏ผ่อ{วZำ$งX๘ชŸน๏Fีœ› ๔ฎ€Vะ+BNfPส ๔Ÿ|cเฯ Ÿ} ~vXิฤ[ม?๙ก/y‹๗บZฯ$งS๘ช_ธช๐๘ฐั*€nœ %6\tw ฑ^ดz{F๒‘๔20Aพฬา๗ มฒ๚ๅี4Ÿe_0ยoX ๋-?ฺจ<8l7๔—ผ๘๙?ฝืํฮ’ฉ`„๔ฬwaแตkฺž/~r*ฑž๘Œฯ[๒ƒๆ%๛,ห‡จิจ้H@™>> hฉํ7”“^5ŠOอไl”Ÿ๕sขGภน่ทiqYูƒ9.=บ_g็ฎx๙7๙™ฝnsลถธื'ฐŸ…฿tไ๑จp9|M=@+& ‚พ’‘ํŒฏbๅz–๎cฌc,}5h~nๅeุฤ+บฯ™;@9/๙dˆ=ํ๏ญ?ZหเP3กfยV๘k๗ฏxึฑฝnk%™ภ ฟ้—žฯฯ†hw QูQ!ฐUกฑžQ์oveฐฏ ฐƒtไeฌภภ‹| ้%บoF๘ณ๕แณ฿๔ฬ๕GT„ํปถ›๐•อ…ฟ๓ร_โO๎u“Iœยo๚•‡o~ ฎก6ะ)„A$J€G*StฐAหZ pา ภฌc$ฯPฤH๛๗-€๏Œภงพ!๚ฯ ่G๙I๐งภ๏x0jl‡€c ?ๆaใ๗`ฏึ2™bg€ะƒ๎ูXธงฃž4U;bฐ™กp7j0จHฟ9ถB5หmu?–ษhDkNฦ๒อ<๕’ค้ฬ>เ!ฎ,ซํ๘2–๎eœํ‹<\BฤY~๚E๚MBไ ฆ๎%ขฤ`ภฎป9‚BC›Xธอ๗๏)๛ ็ฎถฝฝ>Iแ7ฦใเš—ภีU๋ ˆธ5m!’่2ิ%ษ#ด(XrY๎DฉพQ็าX@š—YaอMžล%q>†฿bฅˆ?sธ“p•ภ  TฃAšl…€c ๒ฐg๑•{žV‘‰œAB฿๓S/Eณ~๊ตอ–ฬะ3‚P , 8@nCค!ทฒ‰•/E๋G ื“} uše€|NA๚%Ÿก@ฮ๗G~œF๐“๙ +ะLaฐ๒๒ฃหั>˜่‚uฎ™ฏฟ ๒› ๊เ8‚DํๆŸœ๓ฺU{–V•IœaB—ฤะฌ}๊ต/ขYk•@TQฐ์2”/‘Šr€ค/)ฌgeFส&เ/tYš๙Tศo อ๚/ข^oZ&ฐน3ม$#p้/oรฐœภ หIJtž๕~๊@๘]ฬ„ี,บฉ฿ังจฟดฒหpI๕C๛sM~็kxฟ€๓ 8?~ีฮ/:Fฐ\rmฐ–ๆn—ฟฑืํf'2M>ร…Ž žษ๒ขฟD]\} จ๎~C`7อธwๆ1ะ~Ibฝ;HF™ฦ ฌฒฌสฃ๘‚ธS_—ช7้ฉฐงโฒ<ทb ‘Uข[ ‘a€ฉ1€ปู™L]ฯ 5 4Tw=1 ˆt#@๛ภ˜=เ๐ฝn/;•‰\O„๒ค?ƒŸ อฺ๋ัฌwL s ๚^‚๎ฤ‡ุฉet Œ^„ฤ]P๙ฐญsžŽ!%ถ!๗u™•ทฮง3ซท๓ยฐู,ชH๑ˆไฮ ๋ไฮ1œp‘๖วŸซAพ๎า›ึ-pก฿5 8•;๚ปฝn';•‰\„.๙‘/2๐ฝxๅ?ˆฆz6|}ใ–xัBI6เะฝ  ƒๅœb๔Œ`”ศ4J๗‘ึ8I—–?ๆงวำbv Qี-G/ํ๕Sทdืญ@ฤล๏๎Kg๙™Z`3โฐ์ฆฯwmฯg[&0UจNG=wK&p=˜๙„W ูธ=š๕ื 9ภh6€ฐ–ฌ‰ˆ„๘=ž@ว ,๋ฏC๗^s]„"{ˆ๏BO,๛P.ฮฆใ>=i๔๙่~๊‹H้ัาz'~ฝฅœg8ฯ๐>Z่๛w๋ฎiื]็ธ บะื•~์์wำ๐O{>N =Mr}~หพ กy.P฿ฎ .๊ ยœฦu ๙ัšB0ƒlึž๔ูญ4™Qฏฮoญพด๐ฆ/บCRV ้๎ด฿๘‰q0Xy9ฑJผ‰‰ฅ๕๏F]2šnปป€€YF@ร 3๊ภุชฎ[ฌ๓Eฟ๐ž[๎u{ุฉL เz.๔ˆว)ย?฿ a้h6พ8ฐ.Nฐฐ`=#๑‚~YBกGr6 ‚`p*]วœฐ๖ยฟG\ถ–,+๋ึ๘ผbk้[ซx๕‹ึ฿9t๑9ด์@ฐ…ถG€ป๚ฺx+u๕oj๋ใื;๋L1€„ะeGๆžหoxู๏cซ๚/ ๐TP}vยะฌb?๕:pษ9}฿๚Wแ&]๗tห> bลดVิ/uoC€8ะ}>‘cDษh9@Rˆi]ijฯ1๔‘. œ8‰#วh๗์v!!N๛‡=wC&pz๘cฟเ๘่KŸ‡zใipอS€๚Fฝ บS–"ฐ‚„ฑ+ัฅHL\Œธ2Cฝฒo^) ึ(ƒ‚vฉ0|SUtBด๙ิว˜จ4มuŠ + ฤหw/ˆ*ค~kซk๗iง๛ุ๋็"2)€ ะe๛€Ÿ็ฃG๓๙ใAแว@๕ญZEะ)ƒ~ฉ•€TrB!Hvฐ$6 -:†ชK~2bR t<bDŸภ๗๔ฟ-๔Zงcฺ7ญwœข กแ๛ aะ๐แ&0ผ<ิ4๛เ^?๗‘)ธ„ฏพบยg>๛Px"ะ{PใREl๗ q Œ๎C   ƒ?ฃ๔1HYUว๊wA@pœ—7L&jบ~บ[ฦ.ปŽฺเ_ว:;฿‘.8ุ a๘๕ฟv; €นA่ึ3ิQ7ภvMธvฑพo๕น็]rไสใ{ฌw*“ุgย/}7ยฯิะ|#8๖ศA์9ะฌ 2ญ โ:d๘พ+ F‡Cœ๚ ๔๓๎cB๋ทวืคu#๑จnAใ ฮห40)๘กื‡ศ?ฃ๓ ‚Pปyฒ Mๆ ฐ];\;_ฯ}๑]๗๋g{"2น๛L่q8€#|ไศรญ/ธ'ธy8<จo™ป’…‹ะ#VนY—_๊$D๐RW๕~v๏ร๕๏G์"๐(พ~8o\:ชO}Pฐ]ง๘„ โ‰r’าเG0ฤิ๕6ฐ(7Œaˆท if`๖o๋็zยํaฏO`’ฝ>ย_wล€๐?\[๋&D6 cโ‹›=™y—tŸา‹ฉโ่’ฺฟ=ตัNอํว่ว๑๚ขˆะ๕๕$ใzฺฯล๏~Y๛ž๖w ึŸMh‹˜ืวๆk8^ธำŸ๙ฦ)0ษ C๘%ฏ๛x\Œfq1ˆ8h\๋&Œ ฒๅ‡;ฌŽ6ะงAน๓๓ษ-:?ฟSยo•็oPF„=บ๘\๏@\๏”@.@K๕›ึ๗๔?‚ฟ ภผ&lืฎo|๐;›๎ดืฯ์Der&ษ„~ไ‘เU’+oRื›wsภ]วทq‚๙›ฺฑว‚@ผU‡ๅ๗`/๛F=ˆภ^pCํฯˆ๋ฑห.v๏Qฺsใิ๕0ขทu๓#x0ล8๏2๔ใกพผBใQืhย๚s๗๚yิณ๋˜ไ๚)|ไฺ่๖yณ[แึŽp+ แk™๘f ”็:ๆC >ศอ€เZRธhมเ„€#๐:nJฎ9\]ๆ ท ขmอ{&@4่8IG0€$โHE4|@TะŽ‚€[w แH๛%๕š˜7๓ลฎ๘f๕ํ.ˆบ9๔้ฆ๘ีๆเอ_zแ‘หฎทหdR“ „_๒’ูฑ๚เ๗zฺz ใ๘ฝcžl 4ƒีo€<šฦฃiึะ4กn6อ๕๚‹ท6ฮ{ ๘Q&0ษ Vพ๚?f๏ อ@}็ะ,nฦธB „เ๋ฆ๑ŸASฝฟ†ฟzฑนฦo๚๕ว|zฏฯ๛tสค&ูยฏ=›ธ9ทqt`ksvญรฺ—ฟ๖ศ%ืป—xL2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$“L2ษ$7l๙ใฝ^œŽฦญIENDฎB`‚lemonade-sdk-lemonade-dbde812/docs/flm_npu_linux.html000066400000000000000000000554651516551144000230500ustar00rootroot00000000000000 LLMs on Linux with FastFlowLM - Lemonade Server
Linux NPU Support

LLMs on Linux with FastFlowLM

This article will teach you how to run LLMs on your AMD XDNA 2 NPU on Linux using FastFlowLM. Get set up and then show us what you build!
Date:March 11, 2026
Authors:Lemonade and FastFlowLM contributors
Announcement

Updates for Linux Support

Today, it is possible to run LLMs and Whisper on the AMD XDNA 2 NPU. This solution is made up of:

  • Upstream NPU driver in the Linux 7.0+ kernel (with backports for 6.xx kernels).
  • AMD IRON compiler for XDNA NPUs.
  • FastFlowLM, a lightweight LLM runtime for AMD NPUs, which today has added Linux support.
  • Lemonade, ties everything together with a streamlined user experience.

Getting Started

FastFlowLM

FastFlowLM is a lightweight LLM runtime optimized for AMD NPUs. Today, FastFlowLM is adding support for Ubuntu, Arch, and other distros to enable fast, low-power LLMs on Ryzen™ AI PCs that run Linux.

This article will help you:

  • Understand Linux NPU support status and required platform versions
  • Install the FLM + driver stack for your distribution
  • Validate your setup with flm validate
  • Fix common firmware, driver, and memlock issues
HW Requirements

Supported processors

FastFlowLM on Linux requires an AMD XDNA 2 NPU.

Ryzen AI family Codename Status
Max 300-series Strix Halo Supported
300-series Kraken Point, Strix Point Supported
400-series Gorgon Point Supported
Z2 Extreme Handheld devices Supported

Note: Ryzen AI 7000/8000/200-series chips have XDNA 1, which is not supported.

SW Requirements

Runtime stack

This solution requires specific firmware, kernel version, driver, and runtime software to function. The quickstart guide below will help you install these requirements.

Item Requirement
NPU firmware Version 1.1.0.0 or later
Kernel + driver Kernel 7.0+ with amdxdna, or amdxdna-dkms
Runtime FastFlowLM installed
Memlock limit Must be high enough for NPU execution
Quickstart

Setup by distribution

Select your Linux distribution and follow the exact install path.

Ready to enable Linux NPU LLMs?

Install Lemonade, set up the dependencies described above, then start chatting and building!

lemonade-sdk-lemonade-dbde812/docs/gfx1151_linux.html000066400000000000000000000203411516551144000224650ustar00rootroot00000000000000 Kernel Update Required - Lemonade Server
Kernel Update Required
Your kernel has a critical bug affecting Strix Halo (gfx1151) systems

What's the problem?

Your system has a Strix Halo GPU (gfx1151), but your Linux kernel is missing a critical fix that exports CWSR (Context Wave Save/Restore) properties. Without this fix, ROCm may have incorrect VGPR counts, causing crashes for various workloads.

Technical details:

How to fix this

For Ubuntu Users Recommended

Install the OEM Kernel

Ubuntu provides an OEM kernel with the fix included. Open a terminal and run:

sudo apt update && sudo apt install linux-oem-24.04

After installation, reboot your system for the new kernel to take effect.

For Other Linux Distributions

Upgrade to a Kernel with the CWSR Fix

Upgrade your kernel to version 6.18.4 or later, or a kernel with the CWSR fix backported. The fix exports cwsr_size and ctl_stack_size properties to userspace.

1

Check Current Kernel Version

Open a terminal and run:

uname -r
2

Update Your Kernel

Use your distribution's kernel update process. For example:

  • Fedora/RHEL: sudo dnf update kernel
  • Arch Linux: sudo pacman -Syu linux
  • openSUSE: sudo zypper update kernel-default
3

Reboot

Reboot your system to load the new kernel:

sudo reboot
4

Verify

After rebooting, verify the CWSR properties are exported:

grep -E "cwsr_size|ctl_stack_size" /sys/class/kfd/kfd/topology/nodes/*/properties

You should see both cwsr_size and ctl_stack_size listed.

Need Help?

If you continue to experience issues after updating your kernel:

lemonade-sdk-lemonade-dbde812/docs/index.html000066400000000000000000001103701516551144000212630ustar00rootroot00000000000000 Lemonade: Local AI for Text, Images, and Speech

Refreshingly fast
on GPUs and NPUs

Open source. Private. Ready in minutes on any PC.

Chat
What can I do with 128 GB of unified RAM?
Load up models like gpt-oss-120b or Qwen-Coder-Next for advanced tool use.
What should I tune first?
You can use --no-mmap to speed up load times and increase context size to 64 or more.
Image Generation
A pitcher of lemonade in the style of a renaissance painting
Speech
Hello, I am your AI assistant. What can I do for you today?

Built by the local AI community for every PC.

Lemonade exists because local AI should be free, open, fast, and private.

Join the community

Built on the best inference engines

Works with great apps.

Lemonade is integrated in many apps and works out-of-box with hundreds more thanks to the OpenAI API standard.

Built for practical local AI workflows.

Everything from install to runtime is optimized for fast setup, broad compatibility, and local-first execution.

Native C++ Icon

Native C++ Backend

Lightweight service that is only 2MB.

Install Icon

One Minute Install

Simple installer that sets up the stack automatically.

OpenAI Icon

OpenAI API Compatible

Works with hundreds of apps out-of-box and integrates in minutes.

Auto-config Icon

Auto-configures for your hardware

Configures dependencies for your GPU and NPU.

Multi-engine Icon

Multi-engine compatibility

Works with llama.cpp, Ryzen AI SW, FastFlowLM, and more.

Multiple Models Icon

Multiple Models at Once

Run more than one model at the same time.

Cross-platform Icon

Cross-platform

A consistent experience across Windows, Linux, and macOS (beta).

Built-in app Icon

Built-in app

A GUI that lets you download, try, and switch models quickly.

One local service for every modality.

Point your app at Lemonade and get chat, vision, image gen, transcription, speech gen, and more with standard APIs.

POST /api/v1/chat/completions

Always improving.

Track the newest improvements and highlights from the Lemonade release stream.

Installation Setup
lemonade-sdk-lemonade-dbde812/docs/install_options.html000066400000000000000000000020541516551144000233740ustar00rootroot00000000000000 Lemonade Install Selector
๐Ÿ‹ Installation
lemonade-sdk-lemonade-dbde812/docs/lemonade-cli.md000066400000000000000000000531541516551144000221470ustar00rootroot00000000000000# `lemonade` CLI The `lemonade` CLI is the primary tool for interacting with Lemonade Server from the terminal. It allows you to manage models, recipes, and backends through a simple command-line interface. **Contents:** - [Commands](#commands) - [Global Options](#global-options) - [Options for list](#options-for-list) - [Options for pull](#options-for-pull) - [Options for import](#options-for-import) - [Options for load](#options-for-load) - [Options for run](#options-for-run) - [Options for export](#options-for-export) - [Options for backends](#options-for-backends) - [Options for launch](#options-for-launch) - [Options for scan](#options-for-scan) ## Commands `lemonade` provides these utilities: ### Quick Start | Command | Description | |---------------------|-------------------------------------| | `run MODEL_NAME` | Load a model for inference and open the web app in the browser. See command options [below](#options-for-run). | | `launch AGENT` | Launch an agent with a model. See command options [below](#options-for-launch). | ### Server | Command | Description | |---------------------|-------------------------------------| | `status` | Check if server can be reached. If it is, prints server information. Use `--json` for machine-readable output. | | `logs` | Open server logs in the web UI. | | `backends` | List available recipes and backends. Use `install` or `uninstall` to manage backends. | | `scan` | Scan for network beacons on the local network. See command options [below](#options-for-scan). | ### Model Management | Command | Description | |---------------------|-------------------------------------| | `list` | List all available models. | | `pull MODEL_OR_CHECKPOINT` | Download a registered model, pull a Hugging Face checkpoint, or manually register a `user.*` model with `--checkpoint`/`--recipe`. See command options [below](#options-for-pull). | | `import JSON_FILE` | Import a model from a JSON configuration file. See command options [below](#options-for-import). | | `delete MODEL_NAME` | Delete a model and its files from local storage. | | `load MODEL_NAME` | Load a model for inference. See command options [below](#options-for-load). | | `unload [MODEL_NAME]` | Unload a model. If no model name is provided, unload all loaded models. | | `export MODEL_NAME` | Export model information to JSON format. See command options [below](#options-for-export). | ### Global Flags | Flag | Description | |---------------------|-------------------------------------| | `--help` | Display help information. | | `--help-all` | Display help information for all subcommands. | | `--version` | Print the `lemonade` CLI version. | ## Global Options The following options are available for all commands: | Option | Description | Default | |--------|-------------|---------| | `--host HOST` | Server host address | `127.0.0.1` | | `--port PORT` | Server port number | `13305` | | `--api-key KEY` | API key for authentication | None | These options can also be set via environment variables: - `LEMONADE_HOST` for `--host` - `LEMONADE_PORT` for `--port` - `LEMONADE_API_KEY` or `LEMONADE_ADMIN_API_KEY` for `--api-key` **Examples:** On Linux/macOS: ```bash export LEMONADE_HOST=192.168.1.100 export LEMONADE_PORT=13305 export LEMONADE_API_KEY=your-api-key-here lemonade list ``` On Windows (Command Prompt): ```cmd set LEMONADE_HOST=192.168.1.100 set LEMONADE_PORT=13305 set LEMONADE_API_KEY=your-api-key-here lemonade list ``` On Windows (PowerShell): ```powershell $env:LEMONADE_HOST="192.168.1.100" $env:LEMONADE_PORT="13305" $env:LEMONADE_API_KEY="your-api-key-here" lemonade list ``` **Admin API Key Example:** To use the admin API key (which provides full access including internal endpoints): On Linux/macOS: ```bash export LEMONADE_ADMIN_API_KEY=admin-secret-key lemonade list ``` On Windows (Command Prompt): ```cmd set LEMONADE_ADMIN_API_KEY=admin-secret-key lemonade list ``` On Windows (PowerShell): ```powershell $env:LEMONADE_ADMIN_API_KEY="admin-secret-key" lemonade list ``` ```bash # List all available models lemonade list # Pull a custom model with specific checkpoint lemonade pull user.MyModel --checkpoint main org/model:Q4_K_M --recipe llamacpp # Load a model with custom recipe options lemonade load Qwen3-0.6B-GGUF --ctx-size 8192 # Install a backend for a recipe lemonade backends install llamacpp:vulkan # Export model info to JSON file lemonade export Qwen3-0.6B-GGUF --output model-info.json ``` ## Options for list The `list` command displays available models. By default, it shows all models. Use the `--downloaded` flag to filter for downloaded models only: ```bash lemonade list [options] ``` | Option | Description | Default | |--------------------------------|-------------------------------------|---------| | `--downloaded` | Show only downloaded models | False | ## Options for pull The `pull` command downloads and installs models. The single positional argument can be either: 1. **A registered model name** from the [Lemonade Server registry](https://lemonade-server.ai/models.html), e.g. `Qwen3-0.6B-GGUF`. 2. **A Hugging Face checkpoint** of the form `owner/repo`, with an optional `:variant` suffix, e.g. `unsloth/Qwen3-8B-GGUF` or `unsloth/Qwen3-8B-GGUF:Q4_K_M`. When the variant is omitted (or doesn't match), Lemonade fetches the repository, lists the available quantizations (including sharded folder variants), auto-detects any `mmproj-*.gguf` files for vision models, infers labels from the repo id (`embed`/`rerank`), and presents an interactive menu. 3. **A custom `user.*` model name** when you want to manually register a model with explicit checkpoints, recipe, and optional labels. ```bash lemonade pull MODEL_OR_CHECKPOINT [--checkpoint TYPE CHECKPOINT] [--recipe RECIPE] [--label LABEL] ``` | Option | Description | Required | |--------|-------------|----------| | `MODEL_OR_CHECKPOINT` | Registered model name, or `owner/repo[:variant]` Hugging Face checkpoint | Yes | | `--checkpoint TYPE CHECKPOINT` | Manual registration: add a checkpoint entry. Repeat for multi-component models such as `main` + `mmproj` or `main` + `vae`. | No | | `--recipe RECIPE` | Manual registration: recipe to associate with the new `user.*` model (`llamacpp`, `flm`, `ryzenai-llm`, `whispercpp`, `sd-cpp`, `kokoro`) | No | | `--label LABEL` | Manual registration: add a label to the new model. Repeatable. Valid: `coding`, `embeddings`, `hot`, `reasoning`, `reranking`, `tool-calling`, `vision` | No | **Happy path** Most users only need one of these: ```bash # Pull a registered model from the Lemonade Server registry lemonade pull Qwen3-0.6B-GGUF # Pull a Hugging Face GGUF โ€” interactive variant menu appears lemonade pull unsloth/Qwen3-8B-GGUF # Pull a specific variant directly (no menu) lemonade pull unsloth/Qwen3-8B-GGUF:Q4_K_M # Vision model โ€” mmproj is auto-detected and the `vision` label is auto-applied lemonade pull ggml-org/gemma-3-4b-it-GGUF:Q4_K_M # Sharded variant โ€” all shards in the matching folder are downloaded lemonade pull unsloth/Qwen3-30B-A3B-GGUF:Q4_K_M ``` **Manual registration from the same command** When you need explicit multi-file checkpoints, a non-default recipe, or custom labels, use the same `pull` command with a `user.*` model name plus `--checkpoint` and `--recipe`: ```bash lemonade pull user.NAME --checkpoint TYPE CHECKPOINT [--recipe RECIPE] [--label LABEL] ``` ```bash # Register and pull a custom GGUF model with main checkpoint lemonade pull user.Phi-4-Mini-GGUF \ --checkpoint main unsloth/Phi-4-mini-instruct-GGUF:Q4_K_M \ --recipe llamacpp # Register and pull a vision model with main + mmproj lemonade pull user.Gemma-3-4b \ --checkpoint main ggml-org/gemma-3-4b-it-GGUF:Q4_K_M \ --checkpoint mmproj ggml-org/gemma-3-4b-it-GGUF:mmproj-model-f16.gguf \ --recipe llamacpp # Register a model with multiple labels lemonade pull user.MyCodingModel \ --checkpoint main org/model:Q4_0 \ --recipe llamacpp \ --label coding \ --label tool-calling ``` ## Options for import The `import` command supports two flows: - Import from a local JSON file. - Browse remote recipes from `lemonade-sdk/recipes` and import one interactively. This is useful for importing models with complex configurations that would be cumbersome to specify via command-line options: ```bash lemonade import [JSON_FILE] [options] ``` | Option | Description | Required | |--------|-------------|----------| | `JSON_FILE` | Path to a JSON configuration file | No | | `--directory DIR` | Remote recipes directory to query (e.g., `coding-agents`) | No | | `--recipe-file FILE` | Specific recipe JSON filename from the selected directory | No | | `--skip-prompt` | Run non-interactively (requires `--directory` and `--recipe-file` for remote import) | No | | `--yes` | Alias for `--skip-prompt` | No | **Remote import notes:** - Running `lemonade import` without `JSON_FILE` starts interactive recipe browsing from GitHub. - You can skip recipe import during prompts and continue. - In non-interactive mode, you must provide both `--directory` and `--recipe-file`. - `--recipe-file` is only used for remote recipe import (with `--directory`). **JSON File Format:** The JSON file must contain the following fields: | Field | Type | Description | |-------|------|-------------| | `model_name` | string | The model name (will be prepended with `user.` if not already present) | | `recipe` | string | Inference recipe to use (e.g., `llamacpp`, `flm`, `sd-cpp`, `whispercpp`) | | `checkpoint` | string | Single checkpoint in the format `org/model:variant` | **OR** | | `checkpoints` | object | Multiple checkpoints as key-value pairs (e.g., `{"main": "org/model:Q4_0", "mmproj": "mmproj.gguf"}`) | **Optional fields:** | Field | Type | Description | |-------|------|-------------| | `labels` | array | Array of label strings (e.g., `["reasoning", "coding"]`) | | `recipe_options` | object | Recipe-specific options (e.g., `{"ctx-size": 8192, "llamacpp": "vulkan"}`) | | `image_defaults` | object | Image generation defaults for image models | | `size` | string | Model size description | **Notes:** - The `model_name` field is required and must be a string - The `recipe` field is required and must be a string - Either `checkpoint` (string) or `checkpoints` (object) is required - If both `checkpoint` and `checkpoints` are present, only `checkpoints` will be used - The `id` field can be used as an alias for `model_name` - Unrecognized fields are removed during validation **Examples:** `model.json`: ```json { "model_name": "MyModel", "checkpoint": "unsloth/Qwen3-8B-GGUF:Q4_K_M", "recipe": "llamacpp", "labels": ["reasoning"] } ``` ```bash # Import a model from a JSON file lemonade import model.json # Interactively browse and import a remote recipe lemonade import # Non-interactive remote import lemonade import --directory coding-agents --recipe-file GLM-4.7-Flash-GGUF-NoThinking.json --yes ``` `model-with-multiple-checkpoints.json`: ```json { "model_name": "MyMultimodalModel", "checkpoints": { "main": "ggml-org/gemma-3-4b-it-GGUF:Q4_K_M", "mmproj": "ggml-org/gemma-3-4b-it-GGUF:mmproj-model-f16.gguf" }, "recipe": "llamacpp", "labels": ["vision", "reasoning"] } ``` ```bash # Import a model with multiple checkpoints lemonade import model-with-multiple-checkpoints.json ``` `model-with-id-alias.json`: ```json { "id": "MyModel", "checkpoint": "unsloth/Qwen3-8B-GGUF:Q4_K_M", "recipe": "llamacpp" } ``` ```bash # Import using 'id' as alias for model_name lemonade import model-with-id-alias.json ``` ## Options for load The `load` command loads a model into memory for inference. It supports recipe-specific options that are passed to the backend server: ```bash lemonade load MODEL_NAME [options] ``` ### Recipe-Specific Options The following options are available depending on the recipe being used: #### Llama.cpp (`llamacpp` recipe) | Option | Description | Default | |--------|-------------|---------| | `--ctx-size SIZE` | Context size for the model | `4096` | | `--llamacpp BACKEND` | LlamaCpp backend to use | Auto-detected | | `--llamacpp-args ARGS` | Custom arguments to pass to llama-server (must not conflict with managed args) | `""` | #### FLM (`flm` recipe) | Option | Description | Default | |--------|-------------|---------| | `--ctx-size SIZE` | Context size for the model | `4096` | | `--flm-args ARGS` | Custom arguments to pass to flm serve (e.g., `"--socket 20 --q-len 15"`) | `""` | #### RyzenAI LLM (`ryzenai-llm` recipe) | Option | Description | Default | |--------|-------------|---------| | `--ctx-size SIZE` | Context size for the model | `4096` | #### SD.cpp (`sd-cpp` recipe) | Option | Description | Default | |--------|-------------|---------| | `--sdcpp BACKEND` | SD.cpp backend to use (`cpu` for CPU, `rocm` for AMD GPU) | Auto-detected | | `--sdcpp-args ARGS` | Custom arguments to pass to sd-server (must not conflict with managed args) | `""` | | `--steps N` | Number of inference steps for image generation | `20` | | `--cfg-scale SCALE` | Classifier-free guidance scale for image generation | `7.0` | | `--width PX` | Image width in pixels | `512` | | `--height PX` | Image height in pixels | `512` | #### Whisper.cpp (`whispercpp` recipe) | Option | Description | Default | |--------|-------------|---------| | `--whispercpp BACKEND` | WhisperCpp backend to use | Auto-detected | **Notes:** - Use `--save-options` to persist your configuration for the model - Unspecified options will use the backend's default values - Backend options (`--llamacpp`, `--sdcpp`, `--whispercpp`) are auto-detected based on system capabilities **Examples:** ```bash # Load a model with default options lemonade load Qwen3-0.6B-GGUF # Load a model with custom context size lemonade load Qwen3-0.6B-GGUF --ctx-size 8192 # Load a model and save options for future use lemonade load Qwen3-0.6B-GGUF --ctx-size 4096 --save-options # Load a llama.cpp model with custom backend lemonade load Qwen3-0.6B-GGUF --llamacpp vulkan # Load a llama.cpp model with custom arguments lemonade load Qwen3-0.6B-GGUF --llamacpp-args "--flash-attn on --no-mmap" # Load an image generation model with custom settings lemonade load Z-Image-Turbo --sdcpp rocm --steps 8 --cfg-scale 1 --width 1024 --height 1024 ``` ## Options for run The `run` command is similar to [`load`](#options-for-load) but additionally opens the web app in the browser after loading the model. It takes the same arguments as `load` and opens the URL of the Lemonade Web App in the default browser. **Examples:** ```bash # Load a model and open the web app in the browser lemonade run Qwen3-0.6B-GGUF # Load a model with custom context size and open the web app lemonade run Qwen3-0.6B-GGUF --ctx-size 8192 # Load a model on a different host and open the web app lemonade run Qwen3-0.6B-GGUF --host 192.168.1.100 --port 13305 ``` ## Options for export The `export` command exports model information to JSON format. This is useful for backing up model configurations or sharing model metadata: ```bash lemonade export MODEL_NAME [options] ``` | Option | Description | Required | |--------|-------------|----------| | `--output FILE` | Output file path. If not specified, prints to stdout | No | **Notes:** - The exported JSON includes model metadata such as `model_name`, `recipe`, `checkpoint`, and `labels` - The CLI automatically prepends `user.` to model names if not already present - Unrecognized fields in the model data are removed during export **Examples:** ```bash # Export model info to stdout lemonade export Qwen3-0.6B-GGUF # Export model info to a file lemonade export Qwen3-0.6B-GGUF --output my-model.json # Export and view the JSON output lemonade export Qwen3-0.6B-GGUF --output model.json && cat model.json ``` ## Options for backends The `backends` command lists available recipes and their backends. Use the `install` and `uninstall` subcommands to manage them: ```bash lemonade backends lemonade backends install SPEC [--force] lemonade backends uninstall SPEC ``` | Command | Description | |--------|-------------| | `lemonade backends` | List available recipes and backends | | `lemonade backends install SPEC` | Install a backend. Format: `recipe:backend` (e.g., `llamacpp:vulkan`) | | `lemonade backends uninstall SPEC` | Uninstall a backend. Format: `recipe:backend` (e.g., `llamacpp:cpu`) | | `lemonade backends install SPEC --force` | Bypass hardware filtering and attempt the install anyway | **Notes:** - Available backends depend on your system and the recipe - Use `lemonade backends` to list all available recipes and backends **Examples:** ```bash # List all available recipes and backends lemonade backends # Install Vulkan backend for llamacpp lemonade backends install llamacpp:vulkan # Uninstall CPU backend for llamacpp lemonade backends uninstall llamacpp:cpu # Install FLM backend lemonade backends install flm:npu # Install an otherwise filtered backend lemonade backends install llamacpp:rocm --force ``` ## Options for launch The `launch` command launches an agent and triggers model loading asynchronously. If no model is provided, launch prompts for recipe/model selection before starting the agent: ```bash lemonade launch AGENT [--model MODEL_NAME] [options] ``` | Option/Argument | Description | Required | |-----------------|-------------|----------| | `AGENT` | Agent name to launch. Supported agents: `claude`, `codex`, `opencode` | Yes | | `--model MODEL_NAME` | Model name to launch with. If omitted, you will be prompted to select one. | No | | `--directory DIR` | Remote recipes directory used only if you choose recipe import at prompt | No | | `--recipe-file FILE` | Remote recipe JSON filename used only if you choose recipe import at prompt | No | | `--provider,-p [PROVIDER]` | Codex only: select provider name for Codex config; Lemonade does not read or modify `config.toml` (defaults to `lemonade`) | No | | `--agent-args ARGS` | Custom arguments to pass directly to the launched agent process | `""` | | `--ctx-size SIZE` | Context size for the model | `4096` | | `--llamacpp BACKEND` | LlamaCpp backend to use | Auto-detected | | `--llamacpp-args ARGS` | Custom arguments to pass to llama-server (must not conflict with managed args) | `""` | **Notes:** - The model load request is asynchronous: launch starts the agent immediately while loading continues in the background. - If a model is already provided, launch skips recipe import prompts. - `--directory` and `--recipe-file` are only used for remote recipe import at prompt time. - For local recipe files, run `lemonade import ` first, then launch with the imported model id. - `--api-key` is propagated to the launched agent process. - For `codex`, launch now injects a Lemonade model provider by default so host/port settings are honored. - `--provider` is passed directly to Codex as `model_provider`; provider resolution/errors are handled by Codex. - `--agent-args` is parsed and appended to the launched agent command. - Supported agents: `claude`, `codex`, `opencode` - `opencode` uses an auto-managed config file at `~/.config/opencode/opencode.json`. - When no `--api-key` is provided, the generated opencode provider uses a default `apiKey` value of `lemonade`. **Examples:** ```bash # Launch an agent with default model settings lemonade launch claude --model Qwen3.5-0.8B-GGUF # Launch an agent with custom context size lemonade launch claude --model Qwen3.5-0.8B-GGUF --ctx-size 32768 # Launch an agent with a specific llama.cpp backend lemonade launch codex --model Qwen3.5-0.8B-GGUF --llamacpp vulkan # Launch codex using provider from your Codex config.toml (default provider: lemonade) lemonade launch codex --model Qwen3.5-0.8B-GGUF -p # Launch codex using a custom provider name from your Codex config.toml lemonade launch codex --model Qwen3.5-0.8B-GGUF --provider my-provider # Launch an agent with custom llama.cpp arguments lemonade launch claude --model Qwen3.5-0.8B-GGUF --ctx-size 32768 --llamacpp-args "--flash-attn on --no-mmap" # Pass additional arguments directly to the agent lemonade launch claude --model Qwen3.5-0.8B-GGUF --agent-args "--approval-mode never" # Resume from previous session lemonade launch codex --model Qwen3.5-0.8B-GGUF --agent-args "resume SESSION_ID" lemonade launch claude --model Qwen3.5-0.8B-GGUF --agent-args "--resume SESSION_ID" # Launch and allow optional prompt-driven recipe import using prefilled remote recipe flags lemonade launch claude --directory coding-agents --recipe-file Qwen3.5-35B-A3B-NoThinking.json ``` ## Options for scan The `scan` command scans for network beacons on the local network. Beacons are UDP broadcasts sent by Lemonade Server instances to announce their presence. This command listens for these beacons and displays any discovered servers: ```bash lemonade scan [options] ``` | Option | Description | Default | |--------|-------------|---------| | `--duration SECONDS` | Scan duration in seconds | `30` | **Notes:** - The scan listens on UDP port 13305 for beacon broadcasts - Each beacon must contain `service`, `hostname`, and `url` fields in JSON format - Duplicate beacons (same URL) are automatically filtered out - The scan runs for the specified duration, collecting all beacons during that time **Examples:** ```bash # Scan for beacons for the default duration (30 seconds) lemonade scan # Scan for beacons for a custom duration lemonade scan --duration 5 ``` ## Next Steps The [Lemonade Server API documentation](./server/server_spec.md) provides more information about the endpoints that the CLI interacts with. For details on model formats and recipes, see the [custom model guide](./server/custom-models.md). lemonade-sdk-lemonade-dbde812/docs/man/000077500000000000000000000000001516551144000200375ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/docs/man/man1/000077500000000000000000000000001516551144000206735ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/docs/man/man1/lemonade-server.1000066400000000000000000000011441516551144000240450ustar00rootroot00000000000000.TH LEMONADE-SERVER "1" "November 2025" "lemonade-server" "User Commands" .SH NAME lemonade-server \- Local LLM serving server with GPU and NPU acceleration .SH SYNOPSIS .B lemonade-server [\fI\,OPTIONS\/\fR] .SH DESCRIPTION lemonade-server is the server component of the Lemonade LLM serving system. It provides high-performance local LLM serving with GPU and NPU acceleration support. .SH OPTIONS Run \fBlemonade-server --help\fR for detailed usage information. .SH SEE ALSO .BR lemonade (1) .SH AUTHOR Lemonade SDK developers .SH REPORTING BUGS Report bugs at: https://github.com/lemonade-sdk/lemonade/issues lemonade-sdk-lemonade-dbde812/docs/man/man1/lemonade.1000066400000000000000000000030661516551144000225460ustar00rootroot00000000000000.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. .TH LEMONADE "1" "March 2026" "lemonade version 10.0.0" "User Commands" .SH NAME lemonade \- command line interface .SH SYNOPSIS .B lemonade [\fI\,OPTIONS\/\fR] [\fI\,SUBCOMMAND\/\fR] .SH DESCRIPTION Lemonade CLI \- HTTP client for Lemonade Server .SH OPTIONS .TP \fB\-h\fR,\-\-help Display help information .TP \fB\-\-help\-all\fR Display help information for all subcommands .TP \fB\-v\fR,\-\-version Display program version information and exit .TP \fB\-\-host\fR HOST [127.0.0.1] (Env:LEMONADE_HOST) Server host .TP \fB\-\-port\fR PORT [13305] (Env:LEMONADE_PORT) Server port .TP \fB\-\-api\-key\fR KEY (Env:LEMONADE_API_KEY, Env:LEMONADE_ADMIN_API_KEY) API key for authentication. If both LEMONADE_ADMIN_API_KEY and LEMONADE_API_KEY are set, LEMONADE_ADMIN_API_KEY takes precedence. .SS "Subcommands:" .TP status Check server status .TP list List available models .TP pull Pull/download a model .TP import Import a model from JSON file or remote recipe flow .TP delete Delete a model .TP load Load a model .TP unload Unload a model (or all models) .TP run Load a model and open the webapp in browser .TP backends List available recipes and backends .TP export Export model information to JSON .TP launch Launch an agent with a model .TP scan Scan for network beacons .SH "SEE ALSO" The full documentation for .B lemonade is maintained as a Texinfo manual. If the .B info and .B lemonade programs are properly installed at your site, the command .IP .B info lemonade .PP should give you access to the complete manual. lemonade-sdk-lemonade-dbde812/docs/man/man1/lemond.1000066400000000000000000000011151516551144000222310ustar00rootroot00000000000000.TH LEMOND "1" "November 2025" "lemond" "User Commands" .SH NAME lemond \- Request router for Lemonade LLM serving system .SH SYNOPSIS .B lemond [\fI\,OPTIONS\/\fR] .SH DESCRIPTION lemond handles request routing and load balancing for the Lemonade LLM serving system. It distributes requests across available LLM instances for optimal performance. .SH OPTIONS Run \fBlemond --help\fR for detailed usage information. .SH SEE ALSO .BR lemonade (1), .BR lemonade-server (1) .SH AUTHOR Lemonade SDK developers .SH REPORTING BUGS Report bugs at: https://github.com/lemonade-sdk/lemonade/issues lemonade-sdk-lemonade-dbde812/docs/marketplace.html000066400000000000000000000056001516551144000224430ustar00rootroot00000000000000 Local AI Marketplace - Lemonade

Local AI Marketplace

Apps from our partners and community that work great with Lemonade

Loading apps...

โš ๏ธ

We're Offline

Unable to load marketplace data. Please check your internet connection.

๐Ÿ”

No apps found matching your search.

lemonade-sdk-lemonade-dbde812/docs/models.html000066400000000000000000000110631516551144000214360ustar00rootroot00000000000000 Lemonade Server Models

Models

Browse models and install them with the pull command. You can also register any Hugging Face model into your Lemonade Server with the advanced pull command options.

Loading models from latest tagged release...
lemonade-sdk-lemonade-dbde812/docs/news/000077500000000000000000000000001516551144000202405ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/docs/news/gpt-oss.html000066400000000000000000000354301516551144000225270ustar00rootroot00000000000000 Run OpenAI's gpt-oss locally with Lemonade - Lemonade Server
New Models

Run OpenAI's gpt-oss locally with Lemonade

We're excited to announce that Lemonade now supports OpenAI's gpt-oss models, bringing you the power to run these cutting-edge models locally on your own hardware! ๐ŸŽ‰
Date:August 12, 2025
Authors:Daniel Holanda, Jeremy Fowers, Krishna Sivakumar, Victoria Godsoe
Overview

What is Lemonade?

Lemonade is a local AI runtime that makes it easy to run models like gpt-oss on your own hardware with privacy by default. It is optimized for fast setup, OpenAI API compatibility, and practical performance across common local acceleration stacks.

One Minute Install

Simple setup flow that gets the local stack running quickly.

OpenAI API

Works with many apps out-of-box and integrates in minutes.

Hardware Auto-Setup

Configures dependencies for your GPU and NPU acceleration stack.

Multi-Engine Support

Works with llama.cpp, Ryzen AI SW, FastFlowLM, and more.

Multi-Model Runtime

Run more than one model at the same time on a single machine.

Cross-platform

A consistent experience across Windows, Linux, and macOS.

Model lineup

Choose your gpt-oss model

Use 20B for faster local responsiveness, or 120B for deeper reasoning quality.

gpt-oss-20b Optimized

Optimized for lower latency and local use cases.

Total Parameters21B
Active Parameters3.6B

Perfect for everyday tasks and quick responses.

gpt-oss-120b Production

Production-ready model for high reasoning tasks.

Total Parameters117B
Active Parameters5.1B

Ideal for complex reasoning and advanced applications.

Advanced features

Both models feature OpenAI's sliding window attention and attention sink mechanisms, allowing them to handle long conversations and contexts efficiently while maintaining response quality.

Quickstart

Install and run gpt-oss

Set up Lemonade, download your model, and start chatting locally in minutes.

Install Lemonade

Use these quick download links to get started:

Operating System Downloads
Windows lemonade.msi
Ubuntu lemonade-server_latest_amd64.deb
macOS (beta) Lemonade-latest-Darwin.pkg

Other platforms? See Installation Options for Docker, Snap, Arch, Fedora, and Debian.

Run gpt-oss models

Pull and run the 20B model:

Run gpt-oss-20b
lemonade pull gpt-oss-20b-GGUF
lemonade run gpt-oss-20b-GGUF

For higher reasoning quality, pull and run 120B:

Run gpt-oss-120b
lemonade pull gpt-oss-120b-GGUF
lemonade run gpt-oss-120b-GGUF

Tip: keep models pre-downloaded to avoid startup delays:

Pre-download both
lemonade pull gpt-oss-20b-GGUF
lemonade pull gpt-oss-120b-GGUF
System

System requirements

Recommended memory guidance for each gpt-oss model.

gpt-oss-20b-GGUF

About 13GB RAM is recommended for optimal performance.

gpt-oss-120b-GGUF

Requires significantly more memory for optimal performance.

Ready to get started?

Install Lemonade and run gpt-oss locally with full privacy in just a few commands.

lemonade-sdk-lemonade-dbde812/docs/news/index.html000066400000000000000000000202371516551144000222410ustar00rootroot00000000000000 Lemonade News
Latest News & Updates
Discover insights, tutorials, and the latest developments from Lemonade
lemonade-sdk-lemonade-dbde812/docs/news/lemonade-arcade.html000066400000000000000000000207201516551144000241300ustar00rootroot00000000000000 Introducing Lemonade Arcade: AI-Powered Retro Game Creation
New App

Introducing Lemonade Arcade: AI-Powered Retro Game Creation

We're excited to announce Lemonade Arcade, a new application that transforms your GPU into a creative engine for generating retro-style games using AI! ๐ŸŽ‰
Date:August 22, 2025
Author:Lemonade Team
Overview

What if your GPU could imagine games?

Meet Lemonade Arcade, a new application that transforms your GPU into a creative engine for retro-style games. Just enter a prompt with your vision and within minutes, a playable Python game pops open.

Lemonade Arcade Banner
Lemonade Arcade Demo
AI game generation

Input your idea, such as "asteroids with rainbow lasers," and get a playable game in minutes.

Game library and management

All creations are saved and ready to replay with source code and original prompts.

Lemonade integration

Lemonade Server is integrated to run the LLM locally with strong performance.

Model Stack

Powered by Qwen3-Coder-30B MoE LLM

Under the hood of Lemonade Arcade is a 30 billion parameter language model, served locally via Lemonade Server. This setup provides fast, responsive game generation when your hardware meets the requirements.

Chat-style interface Familiar

Simple chat interface for describing game ideas.

InputNatural-language prompts
OutputPlayable Python games

Just type what you want and watch it come to life.

Workflow

From prompt to playable

Lemonade Arcade combines the simplicity of a chat-style interface with the magic of a game emulator. Instead of emulating old games, it generates entirely new ones from your ideas. Whether you're a developer, hobbyist, or just curious about AI creativity, Lemonade Arcade makes game creation fast, fun, and endlessly remixable.

How it works

1. Describe your game

Enter a simple prompt describing the game you want to create.

2. AI generation

The 30B parameter model generates Python game code.

3. Play instantly

Your game opens and is ready to play within minutes.

Example prompt

Space Invaders Twist
"space invaders with exploding bullets and rainbow colors"
Pong in 2D
"Pong, but the paddles can move in 2D"
Flappy Bird Coins
"Flappy bird with coins for me to collect"
Space Invaders X

Ready to create games with AI?

Transform your ideas into playable games with the power of AI and Lemonade Server.

lemonade-sdk-lemonade-dbde812/docs/publish_website_docs.py000066400000000000000000000071441516551144000240440ustar00rootroot00000000000000# From lemonade repo root, run the following: # pip install -r docs/assets/mkdocs_requirements.txt # Then run this script to publish the documentation to docs/docs/ # python docs/publish_website_docs.py # Standard library imports for file, directory, regex, system, and subprocess operations import os import platform import shutil import re import sys import subprocess def _get_venv_executable(name): """Get an executable path from the venv based on the current Python interpreter.""" python_dir = os.path.dirname(sys.executable) if platform.system() == "Windows": return os.path.join(python_dir, f"{name}.exe") else: return os.path.join(python_dir, name) def main(): # Print the current working directory for debugging print("[INFO] Current working directory:", os.getcwd()) # Define source and destination file paths src = "docs/server/README.md" dst = "docs/index.md" # Check if the source README exists; exit with error if not if not os.path.exists(src): print("[ERROR] docs/server/README.md not found!") sys.exit(1) # Read the source README, making necessary replacements with open(src, "r", encoding="utf-8") as f: readme_content = f.read() # Write the content to the destination index.md with open(dst, "w", encoding="utf-8") as f: f.write(readme_content) print("[INFO] Copied docs/server/README.md to docs/index.md.") # Read the just-written index.md and perform additional link fixes for website publishing print("[INFO] Fixing links in docs/index.md...") with open(dst, "r", encoding="utf-8") as f: content = f.read() # List of (pattern, replacement) tuples for fixing internal documentation links replacements = [ (r"\(\./apps/README\.md\)", r"(./server/apps/README.md)"), (r"\(\./concepts\.md\)", r"(./server/concepts.md)"), (r"\(\./lemonade-cli\.md\)", r"(./lemonade-cli.md)"), (r"\(\./server/configuration\.md\)", r"(./server/configuration.md)"), (r"\(\./server_models\.md\)", r"(https://lemonade-server.ai/models.html)"), (r"\(\./server_spec\.md\)", r"(./server/server_spec.md)"), (r"\(\./server_integration\.md\)", r"(./server/server_integration.md)"), ] for pattern, repl in replacements: content = re.sub(pattern, repl, content) # Write the fully processed content back to index.md with open(dst, "w", encoding="utf-8") as f: f.write(content) # Remove existing docs/docs if it exists if os.path.exists("docs/docs"): print("Removing ", os.path.abspath("docs/docs")) shutil.rmtree("docs/docs") # Build the documentation using mkdocs print("[INFO] Building documentation with mkdocs...") mkdocs_exe = _get_venv_executable("mkdocs") subprocess.run([mkdocs_exe, "build", "--clean"], check=True) # Move the generated site/ directory to docs/docs/, replacing it if it already exists print("[INFO] Moving site/ to docs/docs/...") # Check what mkdocs actually generated if os.path.exists(os.path.abspath("site/docs")): # If mkdocs generated site/docs/, move that content source_dir = os.path.abspath("site/docs") elif os.path.exists(os.path.abspath("site")): # If mkdocs generated site/, move that content source_dir = os.path.abspath("site") else: print("[ERROR] No site directory found after mkdocs build!") sys.exit(1) # Move the correct source directory shutil.move(source_dir, "docs/docs") print(f"[INFO] Moved {os.path.abspath(source_dir)} to docs/docs/") if __name__ == "__main__": main() lemonade-sdk-lemonade-dbde812/docs/self_hosted_runners.md000066400000000000000000000270351516551144000236700ustar00rootroot00000000000000# ๐ŸŒฉ๏ธ Self Hosted Runners ๐ŸŒฉ๏ธ Documentation This page documents how to set up and maintain self-hosted runners for lemonade-sdk. Topics: - [What are Self-Hosted Runners?](#what-are-self-hosted-runners) - [NPU Runner Setup](#npu-runner-setup) - [Maintenance and Troubleshooting](#maintenance-and-troubleshooting) - [Check your runner's status](#check-your-runners-status) - [Actions are failing unexpectedly](#actions-are-failing-unexpectedly) - [Take a laptop offline](#take-a-laptop-offline) - [Creating Workflows](#creating-workflows) - [Capabilities and Limitations](#capabilities-and-limitations) ## What are Self-Hosted Runners? A "runner" is a computer that has installed GitHub's runner software, which runs a service that makes the laptop available to run GitHub Actions. In turn, Actions are defined by Workflows, which specify when the Action should run (manual trigger, CI, CD, etc.) and what the Action does (run tests, build packages, run an experiment, etc.). You can read about all this here: [GitHub: About self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners). ## NPU Runner Setup This guide will help you set up a Ryzen AI laptop as a GitHub self-hosted runner. This will make the laptop available for on-demand and CI jobs that require NPU resources. ### New Machine Setup - Install the following software: - The latest RyzenAI driver ONLY (do not install RyzenAI Software), which is [available here](https://ryzenai.docs.amd.com/en/latest/inst.html#install-npu-drivers) - [VS Code](https://code.visualstudio.com/Download) - [git](https://git-scm.com/downloads/win) - If your laptop has an Nvidia GPU, you must disable it in device manager - Open a PowerShell script in admin mode, and run `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned` - Go into Windows settings: - Go to system, power & battery, screen sleep & hibernate timeouts, and make it so the laptop never sleeps while plugged in. If you don't do this it can fall asleep during jobs. - Search "Change the date and time", and then click "sync" under "additional settings." ### Runner Configuration These steps will place your machine in the `stx-test` pool, which is where we put machines while we are setting them up. In the next section we will finalize setup and then move the runner into the production pool. 1. IMPORTANT: before doing step 2, read this: - Use a powershell administrator mode terminal - Enable permissions by running `Set-ExecutionPolicy RemoteSigned` - When running `./config.cmd` in step 2, make the following choices: - Name of the runner group = `stx` - For the runner name, call it `NAME-stx-NUMBER`, where NAME is your alias and NUMBER would tell you this is the Nth STX machine you've added. - Apply the label `stx-test` as well as a label with your name to indicate that you are maintaining the runner. - Accept the default for the work folder - You want the runner to function as a service (respond Y) - User account to use for the service = `NT AUTHORITY\SYSTEM` (not the default of `NT AUTHORITY\NETWORK SERVICE`) 1. Follow the instructions here for Windows|Ubuntu, minding what we said in step 1: https://github.com/organizations/lemonade-sdk/settings/actions/runners/new 1. You should see your runner show up in the `stx` runner group in the lemonade-sdk org ### Runner Setup These steps will use GitHub Actions to run automated setup and validation for your new runner while it is still in the `stx-test` group. 1. Go to the [lemonade ryzenai test action](https://github.com/lemonade-sdk/lemonade/actions/workflows/test_ryzenai.yml) and click "run workflow". - Select `stx-test` as the runner group - Click `Run workflow` 1. The workflow should appear at the top of the queue. Click into it. - Expand the `Set up job` section and make sure `Runner name:` refers to your new runner. Otherwise, the job may have gone to someone else's runner in the test group. You can re-queue the workflow until it lands on your runner. - Wait for the workflow to finish successfully. 1. Repeat step 1. Wait for it to finish successfully. Congrats, your new runner is working! 1. Go to the stx Runner Group, click your new runner, and click the gear icon to change labels. Uncheck `stx-test` and check `stx`. 1. Done! ## Maintenance and Troubleshooting This is a production system and things will go wrong. Here is some advice on what to do. ### Check your runner's status You can run `Get-EventLog -LogName Application -Source ActionsRunnerService` in a powershell terminal on your runner to get more information about what it's been up to. If there have been any problems recently, they may show up like: - Error: Runner connect error: < details about the connection error > - Information: Runner reconnected - Information: Running Job: < job name > - Information: Job < job name > completed with result: [Succeeded / Canceled / Failed] ### Actions are failing unexpectedly Actions fail all the time, often because they are testing buggy code. However, sometimes an Action will fail because something is wrong with the specific runner that ran the Action. If this happens to you, here are some steps you can take (in order): 1. Take note of which runner executed your Action. You can check this by going to the `Set up job` section of the Action's log and checking the `Runner name:` field. The machine name in that field will correspond to a machine on the [runners page](https://github.com/organizations/lemonade-sdk/settings/actions/runners). 1. Re-queue your job. It is possible that that the failure is a one-off, and it will work the next time on the same runner. Re-queuing also gives you a chance of getting a runner that is in a healthier state. 1. If the same runner is consistently failing, it is probably in an unhealthy state (or you have a bug in your code and you're just blaming the runner). If a runner is in an unhealthy state: 1. [Take the laptop offline](#take-a-laptop-offline) so that it stops being allocated Actions. 1. [Open an Issue](https://github.com/lemonade-sdk/lemonade/issues/new). Assign it to the maintainer of the laptop (their name should be in the runner's name). Link the multiple failed workflows that have convinced you that this runner is unhealthy. 1. Re-queue your job. You'll definitely get a different runner now since you took the unhealthy runner offline. 1. If all runners are consistently failing your workflow, seriously think about whether your code is the problem. ### Take a laptop offline If you need to do some maintenance on your laptop, use it for dev/demo work, etc. you can remove it from the runners pool. Also, if someone else's laptop is misbehaving and causing Actions to fail unexpectedly, you can remove that laptop from the runners pool to make sure that only healthy laptops are selected for work. There are three options: Option 1, which is available to anyone in the `lemonade-sdk` org: remove the `rai300_400` label from the runner. - Workflows use `runs-on: rai300_400` to target runners with the `rai300_400` label. Removing this label from the runner will thus remove the runner from the pool. - Go to the [runners page](https://github.com/organizations/lemonade-sdk/settings/actions/runners), click the specific runner in question, click the gear icon in the Labels section, and uncheck `rai300_400`. - To reverse this action later, go back to the [runners page](https://github.com/organizations/lemonade-sdk/settings/actions/runners), click the gear icon, and check `rai300_400`. Option 2, which requires physical/remote access to the laptop: - In a PowerShell terminal, run `Stop-Service "actions.runner.*"`. - To reverse this action, run `Start-Service "actions.runner.*"`. Option 3 is to just turn the laptop off :) ## Creating Workflows GitHub Workflows define the Actions that run on self-hosted laptops to perform testing and experimentation tasks. This section will help you learn about what capabilities are available and show some examples of well-formed workflows. ### Capabilities and Limitations Because we use self-hosted systems, we have to be careful about what we put into these workflows so that we avoid: - Corrupting the laptops, causing them to produce inconsistent results or failures. - Over-subscribing the capacity of the available laptops Here are some general guidelines to observe when creating or modifying workflows. If you aren't confident that you are properly following these guidelines, please contact someone to review your code before opening your PR. - Place a ๐ŸŒฉ๏ธ emoji in the name of all of your self-host workflows, so that PR reviewers can see at a glance which workflows are using self-hosted resources. - Example: `name: Test Lemonade on NPU and Hybrid with OGA environment ๐ŸŒฉ๏ธ` - Avoid triggering your workflow before anyone has had a chance to review it against these guidelines. To avoid triggers, do not include `on: pull request:` in your workflow until after a reviewer has signed off. - Only map a workflow with `runs on: rai300_400` if it actually requires Ryzen AI compute. If a step in your workflow can use generic compute (e.g., running a Hugging Face LLM on CPU), put that step on a generic non-self-hosted runner like `runs on: windows-latest`. - Be very considerate about installing software on to the runners: - Installing software into the CWD (e.g., a path of `.\`) is always ok, because that will end up in `C:\actions-runner\_work\REPO`, which is always wiped between tests. - Installing software into `AppData`, `Program Files`, etc. is not advisable because that software will persist across tests. See the [setup](#npu-runner-setup) section to see which software is already expected on the system. - Always create new virtual environments in the CWD, for example `python -m venv .venv`. - This way, the virtual environment is located in `C:\actions-runner\_work\REPO`, which is wiped between tests. - Make sure to activate your virtual environment before running any `pip install` commands. Otherwise your workflow will modify the system Python installation! - PowerShell scripts do not necessarily raise errors by programs they call. - That means PowerShell can call a Python test, and then keep going and claim "success" even if that Python test fails and raises an error (non-zero exit code). - You can add `if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }` after any line of script where it is that is particularly important to fail the workflow if the program in the preceding line raised an error. - For example, this will make sure that lemonade installed correctly: 1. pip install -e . 2. if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Be considerate of how long your workflow will run for, and how often it will be triggered. - All workflows go into the same queue and share the same pool of runners. - A good target length for a workflow is 15 minutes. - Be considerate of how much data your workflow will download. - It would be very bad to fill up a hard drive, since Windows machines misbehave pretty bad when their drives are full. - Place your Hugging Face cache inside the `_work` directory so that it will be wiped after each job. - Example: `$Env:HF_HOME=".\hf-cache"` - Place your Lemonade cache directory inside the `_work` directory so that it will be wiped after each job. - Example: Pass the cache dir as the first argument to `lemond`: `lemond .\ci-cache` # License [Apache 2.0 License](../LICENSE) Copyright(C) 2024-2025 Advanced Micro Devices, Inc. All rights reserved. lemonade-sdk-lemonade-dbde812/docs/server/000077500000000000000000000000001516551144000205725ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/docs/server/README.md000066400000000000000000000071701516551144000220560ustar00rootroot00000000000000# Getting Started with Lemonade Server ๐Ÿ‹ Lemonade Server is a server interface that uses the standard Open AI API, allowing applications to integrate with local LLMs. This means that you can easily replace cloud-based LLMs with private and free LLMs that run locally on your own PC's NPU and GPU. Lemonade Server is available as a standalone tool with a [one-click Windows GUI installer](https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade-server-minimal.msi). Installers are also available for [Linux](https://lemonade-server.ai/install_options.html#linux) and [macOS (beta)](https://lemonade-server.ai/install_options.html#macos). ## Intro Video โ–ถ๏ธ [Watch on YouTube](https://www.youtube.com/watch?v=mcf7dDybUco) Once you've installed, we recommend checking out these resources: | Documentation | Description | |---------------|-------------| | [Supported Applications](./apps/README.md) | Explore applications that work out-of-the-box with Lemonade Server. | | [Lemonade Server Concepts](./concepts.md) | Background knowledge about local LLM servers and the OpenAI standard. | | [`lemonade` CLI Guide](../lemonade-cli.md) | Manage models and interact with the server from the command line. | | [Server Configuration](./configuration.md) | Customize environment variables, backend binaries, and server behavior. | | [Models List](https://lemonade-server.ai/models.html) | Browse a curated set of LLMs available for serving. | | [Server Spec](./server_spec.md) | Review all supported OpenAI-compatible and Lemonade-specific API endpoints. | | [Integration Guide](./server_integration.md) | Step-by-step instructions for integrating Lemonade Server into your own applications. | > Note: if you want to develop Lemonade Server itself, you can [install from source](https://lemonade-server.ai/install_options.html). ## Integrate Lemonade Server with Your Application Since Lemonade Server implements the standard OpenAI API specification, you can use any OpenAI-compatible client library by configuring it to use `http://localhost:13305/api/v1` as the base URL. A table containing official and popular OpenAI clients on different languages is shown below. Feel free to pick and choose your preferred language. | Python | C++ | Java | C# | Node.js | Go | Ruby | Rust | PHP | |--------|-----|------|----|---------|----|-------|------|-----| | [openai-python](https://github.com/openai/openai-python) | [openai-cpp](https://github.com/olrea/openai-cpp) | [openai-java](https://github.com/openai/openai-java) | [openai-dotnet](https://github.com/openai/openai-dotnet) | [openai-node](https://github.com/openai/openai-node) | [go-openai](https://github.com/sashabaranov/go-openai) | [ruby-openai](https://github.com/alexrudall/ruby-openai) | [async-openai](https://github.com/64bit/async-openai) | [openai-php](https://github.com/openai-php/client) | ### Python Client Example ```python from openai import OpenAI # Initialize the client to use Lemonade Server client = OpenAI( base_url="http://localhost:13305/api/v1", api_key="lemonade" # required but unused ) # Create a chat completion completion = client.chat.completions.create( model="Llama-3.2-1B-Instruct-Hybrid", # or any other available model messages=[ {"role": "user", "content": "What is the capital of France?"} ] ) # Print the response print(completion.choices[0].message.content) ``` For more detailed integration instructions, see the [Integration Guide](./server_integration.md). lemonade-sdk-lemonade-dbde812/docs/server/apps/000077500000000000000000000000001516551144000215355ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/docs/server/apps/README.md000066400000000000000000000010151516551144000230110ustar00rootroot00000000000000# App Integration Guides This folder contains integration guides for connecting third-party applications to Lemonade Server. ## View the Marketplace For a complete list of compatible apps with links to guides, videos, and more, visit the **[Lemonade Marketplace](https://lemonade-server.ai/marketplace)**. ## Contributing If you've connected Lemonade to a new application and would like to contribute a guide, see our [contribution guide](../../contribute.md) or email us at [lemonade@amd.com](mailto:lemonade@amd.com). lemonade-sdk-lemonade-dbde812/docs/server/apps/ai-dev-gallery.md000066400000000000000000000075431516551144000246720ustar00rootroot00000000000000# AI Dev Gallery with Lemonade Server ## Overview [AI Dev Gallery](https://aka.ms/ai-dev-gallery) is Microsoft's showcase application that demonstrates various AI capabilities through built-in samples and applications. It provides an easy way to explore and experiment with different AI models and scenarios, including text generation, chat applications, and more. AI Dev Gallery has native integration with Lemonade Server, which means it can automatically detect and connect to your local Lemonade instance without manual URL configuration. ## Expectations AI Dev Gallery works well with most models available in Lemonade. The built-in samples are designed to work with various model types and sizes, making it a great tool for testing and exploring different AI capabilities locally. The application provides a user-friendly interface for experimenting with AI models through pre-built scenarios, making it accessible for both beginners and advanced users. ## Setup ### Prerequisites 1. Install Lemonade Server by following the [Lemonade Server Instructions](../README.md) and using the installer .exe. 2. **Important**: Make sure your Lemonade Server is running before opening AI Dev Gallery. ### Install AI Dev Gallery 1. Open the Microsoft Store on Windows. 2. Search for "AI Dev Gallery" by Microsoft Corporation. 3. Click "Install" to download and install the application. Alternatively, you can access AI Dev Gallery directly through [aka.ms/ai-dev-gallery](https://aka.ms/ai-dev-gallery). ### Connect to Lemonade AI Dev Gallery has native integration with Lemonade Server, so no manual configuration is required. The application will automatically detect your running Lemonade Server instance. **Important**: Ensure your Lemonade Server is running before launching AI Dev Gallery. ## Usage AI Dev Gallery provides various built-in applications and samples to explore AI capabilities: ### Quick Start 1. Launch AI Dev Gallery. 2. Navigate to **Samples** โ†’ **Text** โ†’ **Chat** (or another text/code sample). 3. Click on the model selector above the chat window. 4. Select **Lemonade** from the available providers. 5. Choose your preferred model from the list of available models. ### Supported Scenarios AI Dev Gallery supports various AI scenarios through its sample applications with Lemonade integration: **Text Processing**: - **Conversational AI**: Chat and Semantic Kernel Chat for interactive conversations - **Content Generation**: Generate text for various purposes and creative writing - **Language Tasks**: Translation, grammar checking, and paraphrasing - **Text Analysis**: Sentiment analysis and content moderation - **Information Retrieval**: Semantic search and retrieval augmented generation - **Text Enhancement**: Summarization and custom parameter configurations **Code Assistance**: - **Code Generation**: Create code snippets and programs - **Code Analysis**: Explain existing code and understand functionality ### Tips for Best Experience - Start your Lemonade Server before opening AI Dev Gallery - Try different models to see how they perform across various scenarios - Explore different sample categories to understand various AI capabilities - Use the built-in samples as starting points for your own AI experiments ## Troubleshooting ### AI Dev Gallery doesn't detect Lemonade - Ensure Lemonade Server is running and accessible at `http://localhost:13305` - Restart AI Dev Gallery after ensuring Lemonade Server is running ### Models not appearing in the selector - Open `http://localhost:13305` in a browser and make sure to download the models you want to use through the "Model Manager" tab. ## Additional Resources - [AI Dev Gallery Website](https://aka.ms/ai-dev-gallery) - [Lemonade Server Models](https://lemonade-server.ai/models.html) lemonade-sdk-lemonade-dbde812/docs/server/apps/ai-toolkit.md000066400000000000000000000062501516551144000241360ustar00rootroot00000000000000# Microsoft AI Toolkit for VS Code ## Overview The [AI Toolkit for Visual Studio Code](https://learn.microsoft.com/en-us/windows/ai/toolkit/) is a VS Code extension that simplifies generative AI app development by bringing together cutting-edge AI development tools and models from various catalogs. It supports running AI models locally or connecting to remote models via API keys. ## Demo Video โ–ถ๏ธ [Watch on YouTube](https://www.youtube.com/watch?v=JecpotOZ6qo) ## Expectations We have found that most LLMs work well with this application. However, the `Inference Parameters` option is not fully supported, as Lemonade Server currently does not accept those as inputs (see [server_spec.md](../server_spec.md) for details). ## Setup ### Prerequisites 1. Install Lemonade Server by following the [Lemonade Server Instructions](../README.md) and using the installer .exe. ### Install AI Toolkit for VS Code 1. Open the Extensions tab in VS Code Activity Bar. 2. Search for "AI Toolkit for Visual Studio Code" in the Extensions Marketplace search bar. 3. Select the AI Toolkit extension and click install. This will add an AI Toolkit icon to your VS Code Activity Bar. ### Connect Lemonade to AI Toolkit The AI Toolkit now supports "Bring Your Own Model" functionality, allowing you to connect to models served via the OpenAI API standard, which Lemonade uses. 1. Open the AI Toolkit tab in your VS Code Activity Bar. 2. In the right corner of the "My Models" section, click the "+" button to "Add model for remote inference". 3. Select "Add a custom model". 4. When prompted to "Enter OpenAI chat completion endpoint URL" enter: ``` http://localhost:13305/api/v1/chat/completions ``` 5. When prompted to "Enter the exact model name as in the API" select a model (e.g., `Phi-3-Mini-Instruct-Hybrid`) - Note: You can get a list of all models available [here](https://lemonade-server.ai/models.html). 6. Select the same name as the display model name. 7. Skip the HTTP authentication step by pressing "Enter". ## Usage Once you've set up the Lemonade model in AI Toolkit, you can: 1. Use the **AI Playground** tool to directly interact with your added model. 2. Use the **Prompt Builder** tool to craft effective prompts for your AI models. 3. Use the **Bulk Run** tool to compute responses for custom datasets and easily visualize those responses on a table format. 4. Use the **Evaluation** tool to quickly assess your model's coherence, fluency, relevance, and similarity, as well as to compute BLEU, F1, GLEU, and Meteor scores. ## Additional Resources - [AI Toolkit for VS Code Documentation](https://learn.microsoft.com/en-us/windows/ai/toolkit/) - [AI Toolkit GitHub Repository](https://github.com/microsoft/vscode-ai-toolkit) - [Bring Your Own Models on AI Toolkit](https://techcommunity.microsoft.com/blog/azuredevcommunityblog/bring-your-own-models-on-ai-toolkit---using-ollama-and-api-keys/4369411) lemonade-sdk-lemonade-dbde812/docs/server/apps/anythingLLM.md000066400000000000000000000141631516551144000242520ustar00rootroot00000000000000 # Running agents locally with Lemonade and AnythingLLM ## Overview [AnythingLLM](https://github.com/Mintplex-Labs/anything-llm) is a Docker and Desktop application that allows you to leverage agents, RAG, and more with your local LLMs. It comes pre-packaged with everything you need to get started with local LLMs for productivity. AnythingLLM supports the OpenAI-compatible API interface, allowing easy integration with local servers like Lemonade! This guide will help you configure AnythingLLM to use Lemonade's OpenAI-compatible server, and utilize the powerful `@agent` capability to interact with documents, webpages, custom tools, and more. ## AnythingLLM - Docker vs Desktop Docker and Desktop are two different ways to run AnythingLLM. Both are powerful in their own ways and both are absolutely local-first for any features. ### You want to run AnythingLLM in Docker - You want to run AnythingLLM in a containerized environment - You want to support multiple users on the same machine - You want admin access over all users to curate documents and agent skills that are available during inference ### You want to run AnythingLLM in Desktop - You want a "one-click" or "zero configuration" experience to get started using AnythingLLM for your own use cases - Want your model to be able to access knowledge on your own machine (file search, web-browsing, etc.) - Want to have an assistant that "lives" across your entire operating system all powered by your local LLMs via Lemonade on optimized hardware. ## Setup ### Prerequisites 1. Install Lemonade Server by following the [Lemonade Server Instructions](../README.md) and using the installer .exe. 2. Install and set up AnythingLLM from their [Documentation](https://docs.anythingllm.com/installation-docker/quickstart) or [Desktop Installer](https://anythingllm.com/desktop). ### Configure AnythingLLM to Use Lemonade During onboarding or from the "LLM" submenu on the settings sidebar you can set `Lemonade` as your LLM provider. This is the preferred way to get started as it will automatically configure everything for you and provide a point-and-click interface to download and configure your Lemonade LLMs.

Setting up AnythingLLM to use Lemonade on the in-app settings page.
Setting up AnythingLLM to use Lemonade on the in-app settings page
> AnythingLLM will automatically detect your Lemonade Server if running on the same machine. Otherwise, you can manually input the server URL from Lemonade Server's settings page From this page, you can also search and select from all available Lemonade models based on your hardware and preferences. You can also configure the context size as well as managed the models that are available to you through Lemonade without ever needing to leave the AnythingLLM app. > AnythingLLM also supports running Lemonade models for your Embedder to run on your optimized hardware! ## Sending chats to Lemonade In AnythingLLM, we will automatically detect if your model is capable of native tool calling. This gives you the ability to leverage the "@agent" mode to its maximum potential. If your model is not capable of native tool calling, you can still use the "@agent" mode, but you may have worse performance. ### Overview Agents are capable of scraping websites, listing and summarizing documents, searching the web, creating charts, and even saving files to your desktop or their own memory. To start an agent session, simply go to any workspace and type `@agent `. To exit the session, just type `exit`. ### Agent Skills You may turn on and off specific `Agent Skills` by going to your `Workspace Settings` โ†’ `Agent Configuration` โ†’ `Configure Agent Skills` or toggling them from the prompt input box.

Toggling agent skills from the prompt input box.
Toggling agent skills from the prompt input box
Available agent skills include: * RAG & long-term memory * View and summarize documents * Scrape Websites * Generate & save files to browser * Generate Charts * Web Search * SQL Connector You can always use the built in no-code [Agent Flow builder](https://docs.anythingllm.com/agent-flows/overview), [create your own custom skills](https://docs.anythingllm.com/agent/custom/introduction), or [use MCPs](https://docs.anythingllm.com/mcp-compatibility/overview) to have powerful and flexible agentic capabilities with your local models. ### Examples Here are some examples on how you can interact with Anything LLM agents: - **Rag & long-term memory** - `@agent My name is Dr Lemon. Remember this in our next conversation` - Then, on a follow up chat you can ask `@agent What is my name according to your memory?` - **Scrape Websites** - `@agent Scrape this website and tell me what are the two ways of installing lemonade https://github.com/lemonade-sdk/lemonade/blob/main/docs/server/README.md` - **Web Search** (enable skill before trying) - `@agent Search the web for the best place to buy shoes` AnythingLLM also supports complex multi-step agentic tool calling. You can do this with simple natural language. > "@agent Look online for information about the AMD Lemonade SDK and tell me how many GitHub stars it has"

Complex multi-step agentic tool calling with simple natural language.
Complex multi-step agentic tool calling with simple natural language
You can find more details about agent usage [here](https://docs.anythingllm.com/agent/usage). ## Additional Resources - [AnthingLLM Website](https://anythingllm.com/) - [AnythingLLM GitHub](https://github.com/Mintplex-Labs/anything-llm) - [AnythingLLM Documentation](https://docs.anythingllm.com/) lemonade-sdk-lemonade-dbde812/docs/server/apps/claude-code.md000066400000000000000000000040011516551144000242170ustar00rootroot00000000000000# Claude Code Claude Code is Anthropic's coding agent CLI. With Lemonade, you can run Claude Code against local models through Lemonade's Anthropic-compatible API. This guide focuses on the most common launch flows. ## Prerequisites 1. Install Claude Code: ```bash curl -fsSL https://claude.ai/install.sh | bash ``` or: ```bash npm install -g @anthropic-ai/claude-code ``` 2. Make sure Lemonade Server is running (`lemond`). ## Launch Claude Code with Lemonade Use: ```bash lemonade launch claude [options] ``` Lemonade automatically configures Claude Code to use your local server. ## Use Case 1: First-time user (discover + import + launch) If you are not sure which model to use yet, start with: ```bash lemonade launch claude ``` You will get an interactive menu where you can: - Select a recipe to import and launch. - Browse downloaded models. - Browse recommended llama.cpp models (download may be required), then launch. All remote recipes in this flow are sourced from: `https://github.com/lemonade-sdk/recipes` ## Use Case 2: You already know the model If you already downloaded a model or already imported the recipe, skip the interactive flow: ```bash lemonade launch claude -m Qwen3.5-35B-A3B-GGUF ``` Equivalent long form: ```bash lemonade launch claude --model Qwen3.5-35B-A3B-GGUF ``` When `--model` is provided, launch goes straight to starting the agent and loading that model. ## Passing Claude arguments with `--agent-args` You can pass any extra Claude CLI flags through Lemonade: ```bash lemonade launch claude --model Qwen3.5-35B-A3B-GGUF --agent-args "--approval-mode never" ``` Resume a previous Claude session: ```bash lemonade launch claude --agent-args="--resume 6a670b84-ac78-47e0-8c2a-c3e85efdd979" ``` If Claude supports a flag, you can pass it through `--agent-args`. ## Related CLI Docs For more launch examples and full option details, see: `docs/lemonade-cli.md` For Claude Code product details, see Anthropic's docs: https://docs.anthropic.com/en/docs/agents-and-tools/claude-code lemonade-sdk-lemonade-dbde812/docs/server/apps/codeGPT.md000066400000000000000000000055171516551144000233540ustar00rootroot00000000000000# CodeGPT with VS Code ## Overview [CodeGPT Chat](https://codegpt.co/) is an AI-powered chatbot designed to assist developers with coding tasks directly within their preferred integrated development environments (IDEs), for example, VS Code. ## Expectations We have found that the `Qwen-1.5-7B-Chat-Hybrid` model is the best Hybrid model available for coding. It is good at chatting with a few files at a time in your codebase to learn more about them. It can also make simple code editing suggestions pertaining to a few lines of code at a time. However, we do not recommend using this model for analyzing large codebases at once or making large or complex file edits. ## Setup ### Prerequisites 1. Install Lemonade Server by following the [Lemonade Server Instructions](../README.md) and using the installer .exe. ### Install CodeGPT in VS Code > The following instructions are based off CodeGPT provided instructions found [here](https://docs.codegpt.co/docs/tutorial-basics/installation). 1. Open the Extensions tab in VS Code Activity Bar. 1. Search "CodeGPT: Chat & AI Agents" in the Extensions Marketplace search bar. 1. Select the CodeGPT extension and click install. This will add a CodeGPT tab to your VS Code Activity Bar. ### Add Lemonade Server to CodeGPT > Note: The following instructions are based on instructions from CodeGPT found [here](https://docs.codegpt.co/docs/tutorial-ai-providers/custom).
  1. Open the CodeGPT tab in your VS Code Activity Bar.
  2. Sign Up or Sign into your account.
  3. In the model dropdown menu and click "View More".
  4. Select the tab: "LLMs Cloud model"
  5. Under "All Models", set the following:
    FieldValue
    Select Provider:Custom
    Select Model: Qwen-1.5-7B-Chat-Hybrid
  6. Click "Change connection settings" and enter the following information:
    FieldValue
    API Key-
    Custom Linkhttp://localhost:13305/api/v1/api/v1
## Usage > Note: see the CodeGPT [user guide](https://docs.codegpt.co/docs/intro) to learn about all of their features. To try out CodeGPT: - Open the CodeGPT tab in your VS Code Activity Bar, and in the chat box, type a question about your code. Use the `#` symbol to specify a file. - Example: "What's the fastest way to install lemonade in #getting_started.md?" - Use /Fix to find and fix a minor bug. - Use /Document to come up with docstrings and comments for a file. - Use /UnitTest to make a test file. lemonade-sdk-lemonade-dbde812/docs/server/apps/continue.md000066400000000000000000000231711516551144000237070ustar00rootroot00000000000000# Continue Coding Assistant [Continue](https://www.continue.dev/) provides open-source Integrated Development Environment (IDE) extensions, such as for [Visual Studio Code](https://code.visualstudio.com/) and [JetBrains](https://www.jetbrains.com/ides/), and an open-source CLI that lets developers leverage custom AI coding agents. This guide walks through how to use Lemonade Server with the Continue VS Code extension for code generation, editing, and chat capabilities, all running locally on your AMD PC. ## Prerequisites Before you start, make sure you have the following: ### Software Requirements - **IDE**: [Visual Studio Code (v1.80+)](https://code.visualstudio.com/) or another supported IDE. - **Lemonade Server**: Installed and set up using the [Getting Started guide](https://lemonade-server.ai/docs/server/). - **Lemonade Server Running**: The server should be running at `http://localhost:13305`. If you change the port in Lemonade Server (e.g., to 8020, 8040, etc.), you'll need to update the API Base URL in Continue's configuration to match the same port. - **Model Downloaded**: At least one model from the [supported models list](https://lemonade-server.ai/models.html) must be installed locally. ### Hardware Requirements For best results, a code-tuned model with at least 20B parameters is required. To run such a model: * **Minimum spec**: PC with an integrated GPU (Ryzenโ„ข AI 7000-series or newer) and 64 GB system RAM. * **Recommended specs**: * PC with a discrete GPU that has 16 GB VRAM or greater (Radeonโ„ข 7800 XT or newer). * Strix Halo PC with 64 GB System RAM or greater. ## Setup ### Configuring Lemonade Server with Continue 1. **Install Models Locally** - Use the Model Manager or [`lemonade` CLI](https://lemonade-server.ai/docs/lemonade-cli/) to download your desired model, for example: ```bash lemonade pull ``` _Example downloading Qwen3-Coder:_ ```bash lemonade pull Qwen3-Coder-30B-A3B-Instruct-GGUF ``` 2. **Start Lemonade Server**: Ensure Lemonade Server is running at `http://localhost:13305`. The server starts automatically after installation. You can verify with `lemonade status`. 3. **Verify Model is Loaded**: Use the Model Manager or tray icon to confirm your model is loaded and ready. Continue will automatically detect Lemonade Server running on localhost. ### Setting Up Continue Extension in VS Code 1. **Go to Extensions Marketplace**: In VS Code, click the Extensions icon in the Activity bar (default is on the left). 2. **Add "Continue"**: Type "Continue" in the search box. Click "Install" on the Continue extension entry. _Example marketplace screen:_ ![Continue Extension in VS Code Marketplace](https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/guides/continue/continue_vscode_marketplace.png) 3. **Open Continue in VS Code**: After installation completes, the Continue logo appears in the Activity bar. Click it to open the extension. 4. **Add Lemonade Server Provider**: Click the model dropdown menu in the Continue sidebar, then select "Add Chat Model". Choose "Lemonade Server" from the list of available providers. Continue will set the default address to `http://localhost:13305`, but it can be changed to match a different setup. _Example configuration screen:_ ![Add Lemonade Provider](https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/guides/continue/continue_add_lemonade_provider.png) 5. **Select Your Model**: Once Lemonade Server is added, use the drop-down menu to select the model you downloaded earlier (e.g., `Qwen3-Coder-30B-A3B-Instruct-GGUF`). _Example model selection:_ ![Model Selection Dropdown](https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/guides/continue/continue_model_selection_dropdown.png) ## Working with Continue.dev Continue provides three interaction modes for different development tasks: 1. **Chat**: Code explanations, debugging discussions, architecture planning 2. **Plan**: Provides a safe environment with read-only tools for exploring code and planning changes 3. **Agent**: Multi-file refactoring, large-scale changes across projects ![Continue Modes Interface](https://raw.githubusercontent.com/lemonade-sdk/assets/refs/heads/main/docs/guides/continue/continue_extension_modes.png) See the [Continue Documentation](https://docs.continue.dev/getting-started/overview) for detailed descriptions. ## Examples ### Example 1: Chat Mode - Building an Asteroids Game In this example, we'll use `Qwen3-Coder-30B-A3B-Instruct-GGUF` model to build a Python game. **Input**: I want to create an asteroids game using PyGame. What guidelines should I follow in the code to do so? ![Continue Chat Asteroids](https://github.com/lemonade-sdk/assets/blob/main/docs/guides/continue/continue_chat_asteroids01.png?raw=true) The model provides a basic framework for an Asteroids game. You can then prompt it to provide you some sample code to get started. **Input**: Provide me a basic implementation to get started. ![Continue Chat Asteroids Example Code](https://github.com/lemonade-sdk/assets/blob/main/docs/guides/continue/continue_chat_asteroids02.png?raw=true) In the top-right corner, you can click the "Create file" to move the code from the chat window to a Python file and save it. To run, install `pygame` and execute the code with `python main.py`. ### Example 2: Plan Mode - Analysis of the Game In this example, we'll use Plan mode to have the LLM analyze your code and provide feedback. Plan mode reviews your code and suggests improvements, but does not modify your files. **To use Plan mode with large files, increase Lemonade Server's context size:** 1. **Load the model with a higher context size**: Open a terminal and run: ```bash lemonade load --ctx-size 8192 ``` For persistent changes, see [Server Configuration](../configuration.md). 3. **Use Plan mode in VS Code**: Select the "Plan" option in Continue, enter your prompt and press Alt+Enter to include the currently active file as context. **Input**: What improvements could be made to this game? ![Continue Plan Asteroids](https://github.com/lemonade-sdk/assets/blob/main/docs/guides/continue/continue_plan_asteroids.png?raw=true) ### Example 3: Agent Mode - Improving the Game Lastly, we'll use Agent Mode to take action to change the code to implement improvements. ![Continue Agent Asteroids](https://github.com/lemonade-sdk/assets/blob/main/docs/guides/continue/continue_agent_asteroids.png?raw=true) Here, we can see that the agent edited the code in `main.py` to improve the gameplay and add colors. ## Best Practices ### Setup & Configuration - **Install Lemonade Server**: Follow the [setup guide](https://lemonade-server.ai/docs/server/) to install and configure Lemonade Server before you begin development. - **Download Models Locally**: Use `lemonade pull ` to install models you want to use. Refer to the [supported models list](https://lemonade-server.ai/models.html) for available options. - **Pre-load Models**: Start Lemonade Server and load your models before coding sessions. This can easily be done using the Lemon tray icon and `Load`. - **Increase Context Size for Agent Mode**: For large code changes with GGUF models, load the model with a higher context size: ```bash lemonade load --ctx-size 8192 ``` - **Customize Scoping**: See [Continue Customization](https://docs.continue.dev/customization/overview) for tips on effective model configuration and scoping. ### Development Workflow - **Start New Conversations for Each Feature**: Begin a fresh chat for every new feature or task. Clear chat history when switching topics to keep interactions focused. - **Keep Prompts Focused**: Only include the code and context relevant to your current task. This helps the model provide accurate and useful responses. - **Write Clear, Detailed Prompts**: Structure your requests with a clear task description, specific requirements, and any technical constraints. - **Use Agent Mode for Multi-File Changes**: Invoke agent mode with the `@` symbol to perform refactoring or changes across multiple files. - **Be Specific in Your Requests**: Move from broad prompts ("Create a game") to detailed ones ("Create an Asteroids game in Python using Pygame, under 300 lines, with ship controls and asteroid splitting"). - **Iterate and Test Frequently**: Generate an initial implementation, test it right away, and refine with targeted follow-up prompts. - **Leverage Unlimited Iterations**: With local models, you can iterate as many times as needed for continuous improvement. ## Common Issues **Model not appearing in Continue** - Make sure Lemonade Server is running and the model is loaded locally. - Double-check the [supported models list](https://lemonade-server.ai/models.html) and install any missing models with: ```bash lemonade pull ``` **Slow response times** - Pre-load your model before starting coding sessions. - Check your system's available RAM and close unused applications to free up resources. **Missing error handling in generated code** - In your prompt, explicitly request: "with comprehensive error handling" to ensure the model adds proper error checks. **Inconsistent code style** - Provide a sample or example of your desired code style in your prompt. The model will use this as a reference for formatting. ## Resources - [Lemonade Server Setup Guide](https://lemonade-server.ai/docs/server/) - [Lemonade Server Supported Models](https://lemonade-server.ai/models.html) - [Lemonade Applications](https://lemonade-server.ai/docs/server/apps/) - [Continue Documentation](https://docs.continue.dev) lemonade-sdk-lemonade-dbde812/docs/server/apps/lemon-zest.md000066400000000000000000000072211516551144000241560ustar00rootroot00000000000000# Lemon Zest [Lemon Zest](https://github.com/phqen1x/lemon-zest) is a local, AI-powered image editor for object removal and inpainting. Using **Flux-2-Klein-4B** on a **Lemonade Server**, it provides a near-instant "Magic Eraser" experience โ€” all running on your own hardware. ![License](https://img.shields.io/badge/license-MIT-blue) ## Screenshots

Splash screen of Lemon Zest when you first open the application.
Splash screen of Lemon Zest when you first open the application.
Lemon Zest inpainting selected regions of an image.
Lemon Zest inpainting selected regions of an image.
## Features - **Multiple Selection Tools** โ€” Rectangle (R), Circle (C), Lasso (L), Brush (B), and Flood Fill (F) with adjustable tolerance - **AI Inpainting** โ€” Remove objects or fill regions using a custom prompt, with adjustable strength and step count - **Crop-to-Mask Optimization** โ€” Only the masked region is sent for inference, significantly reducing processing time - **Superimpose** โ€” Blend external images onto your canvas with optional prompt-guided blending - **Undo/Redo** โ€” Full history with up to 20 states (Ctrl+Z / Ctrl+Y) - **Zoom & Pan** โ€” Scroll wheel zoom with multiple preset levels (25%โ€“400%) - **Drag & Drop** โ€” Load images by dragging them onto the window - **Format Support** โ€” PNG, JPG, JPEG, BMP, and WebP ## Prerequisites Lemon Zest requires a running **Lemonade Server** to perform inpainting. The server handles model loading and inference locally. 1. Install [Lemonade Server](https://github.com/lemonade-sdk/lemonade) following its documentation. 2. Start the server: ```bash lemonade-server run Flux-2-Klein-4B ``` The server will listen on `http://localhost:13305`. The Flux model will be downloaded automatically on first launch. > **Note:** The Lemonade Server must be running before you start Lemon Zest. The app will show a "Connecting to server..." overlay until the server is available. ## Installation ### Linux (Snap) Install from the Snap Store: ```bash sudo snap install lemon-zest ``` Or build the snap locally: ```bash sudo snap install snapcraft --classic snapcraft sudo snap install lemon-zest_*.snap --dangerous ``` ### Linux (From Source) ```bash git clone https://github.com/phqen1x/lemon-zest.git cd lemon-zest npm install npm start ``` ### Windows #### From Release Download the latest installer (`.exe`) or portable build from the [Releases](https://github.com/phqen1x/lemon-zest/releases) page. - **NSIS Installer** โ€” Standard Windows installer with custom install path - **Portable** โ€” Standalone executable, no installation required #### From Source ```bash git clone https://github.com/phqen1x/lemon-zest.git cd lemon-zest npm install npm start ``` ## Building Packages ### Linux ```bash npm install npx electron-builder --linux dir ``` The unpacked app will be in `dist/linux-unpacked/`. ### Windows ```bash npm install npx electron-builder --win ``` This produces both an NSIS installer and a portable executable in `dist/`. ## Keyboard Shortcuts | Shortcut | Action | |----------|--------| | R | Rectangle select | | C | Circle select | | L | Lasso select | | B | Brush tool | | F | Flood fill | | Ctrl + A | Select all | | Ctrl + Z | Undo | | Ctrl + Y | Redo | | Ctrl + S | Save As | | Ctrl + = | Zoom in | | Ctrl + - | Zoom out | | Ctrl + 0 | Reset zoom | | Escape | Cancel inpainting | | F12 | Toggle DevTools | ## License [MIT](LICENSE) lemonade-sdk-lemonade-dbde812/docs/server/apps/mindcraft.md000066400000000000000000001002061516551144000240250ustar00rootroot00000000000000# Mindcraft ## Overview Mindcraft is an open-source project that creates Minecraft bots powered by large language models (LLMs) to engage with the game and its players. This readme will demonstrate how to integrate Lemonade to use local LLMs with Mindcraft. ## Expectations We found the `Qwen-1.5-7B-Chat-Hybrid` model to be the most effective for this task, delivering fast responses with higher accuracy. However, as a smaller model running locally, with a limited context length, it may occasionally struggle with certain requestsโ€”for instance, it might attempt to build a structure, but the result may not be correct. For more detailed information, please refer to the [Data Insights](#data-insights) section. ## Setup ### Prerequisites 1. Install Lemonade Server by following the [Lemonade Server Instructions](../README.md) and using the installer .exe. 1. Obtain a copy of [Minecraft](https://www.minecraft.net/en-us) from Microsoft. 1.20.4 of the JAVA Edition is required for this. You can obtain that version [by following these instructions](https://www.minecraft.net/en-us/article/minecraft-java-edition-1-20-4). 1. Install [Node.js](https://nodejs.org/en/download) (at least v14). ### Environment Setup Clone [Mindcraft](https://github.com/kolbytn/mindcraft) from GitHub: * `git checkout 07ea071ac3b0d4954d62b09d881c38a06bc2a589`: this will ensure the code base is equal to where this test was performed. In the clone of the `mindcraft` repository: * Rename the file `keys.example.json` to `keys.json`. You do not need to edit the contents of this JSON file. * Update `keys.json` by adding any text to the value of `OPENAI_API_KEY`. ```json { "OPENAI_API_KEY": "", "OPENAI_ORG_ID": "", "GEMINI_API_KEY": "", "ANTHROPIC_API_KEY": "", "REPLICATE_API_KEY": "", "GROQCLOUD_API_KEY": "", "HUGGINGFACE_API_KEY": "", "QWEN_API_KEY": "", "XAI_API_KEY": "", "MISTRAL_API_KEY": "", "DEEPSEEK_API_KEY": "", "NOVITA_API_KEY": "", "OPENROUTER_API_KEY": "" } ``` * In a terminal/command prompt, run `npm install` from the cloned Mindcraft directory. * Replace the contents of the file `andy.json` with the following: ```json { "name": "Andy", "model": { "model": "Qwen-1.5-7B-Chat-Hybrid", "url": "http://localhost:13305/api/v1", "params": { "temperature": 0.5 } }, "modes": { "self_preservation": true, "unstuck": true, "cowardice": false, "self_defense": true, "hunting": true, "item_collecting": true, "torch_placing": true, "elbow_room": true, "idle_staring": true, "cheat": true } } ``` * Find the line in `src/models/prompter.js` that says: ```javascript else if (profile.model.includes('gpt') || profile.model.includes('o1')|| profile.model.includes('o3')) ``` * ... and replace it with the following: ```javascript else if ([ 'Qwen-1.5-7B-Chat-Hybrid', 'Llama-3.2-1B-Instruct-Hybrid' ].includes(profile.model) || profile.model.includes('gpt') || profile.model.includes('o1') || profile.model.includes('o3')) ``` ### Launching Everything * Start Lemonade Server by double-clicking the desktop icon ๐Ÿ‹. * Start a Minecraft world and open it to LAN on localhost port `55916`. * This is done by pressing the ESC button to open the menu, then click "Open to LAN" and enter the Port Number: `55916`. * Click "Start LAN World". For instructions on how Open to LAN, see the section "Hosting a LAN Server" for the JAVA Edition in this [wiki article](https://minecraft.wiki/w/Tutorial:Setting_up_a_LAN_world). * Run `node main.js` from the installed directory. * In Minecraft, to give the agent commands, press `t` and enter the command. For example: * "come here" * "hunt pigs" - you and the agent must be close to some pigs to do this. ### Model Configurations Lemonade models tested: * Llama-3.2-1B-Instruct-Hybrid * **Qwen-1.5-7B-Chat-Hybrid ๐Ÿ‘** ## Challenges and Observations 1. The current MindCraft configuration has a tendency to send very large context that is resent. The context will include examples of behaviors that may not be necessary, generating over 2100 input tokens for simple commands. Further testing would be required to understand model behavior based on reduced context size. 1. Frequent token limit breaches resulting in timeouts or incomplete responses due to the aforementioned context size problem. Once the token maximum content ceiling is raised, further testing would be prudent using the same `Qwen-1.5-7B-Chat-Hybrid` model as a baseline and using it to test other models such as DeepSeek and Llama variants. content 1. High GPU resource consumption during model inference, impacting system performance. ## Results The `Qwen-1.5-7B-Chat-Hybrid` model showed the most potential, responding to commands and attempting construction tasks, though with limited accuracy. ## Recommendations 1. Optimize examples sent to Lemonade for conciseness to reduce token usage. 1. Reduce input context to prevent model overload. ## Data Insights The following are examples of requests made by the Mindcraft software to the Lemonade Server and how the tokens were interpreted. These examples are taken from an initial game stage, including the first request sent to the Lemonade Server and a subsequent user chat that says, "come here." The purpose is to show how large the context is that is being sent. This could be optimized for more efficient performance and results. * Initial Payload ```json {"model":"Qwen-1.5-7B-Chat-Hybrid","messages":[{"role":"system","content":"You are a playful Minecraft bot named LLama that can converse with players, see, move, mine, build, and interact with the world by using commands. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. This is extremely important to me, take a deep breath and have fun :)\n\n\nSTATS\n- Position: x: 6.50, y: -60.00, z: 28.50\n- Gamemode: creative\n- Health: 20 / 20\n- Hunger: 20 / 20\n- Biome: plains\n- Weather: Clear\n- Block Below: grass_block\n- Block at Legs: air\n- Block at Head: air\n- First Solid Block Above Head: none\n- Time: Afternoon- Current Action: Idle\n- Nearby Human Players: Transpier\n- Nearby Bot Players: None.\nAgent Modes:\n- self_preservation(ON)\n- unstuck(ON)\n- cowardice(ON)\n- self_defense(ON)\n- hunting(ON)\n- item_collecting(ON)\n- torch_placing(ON)\n- elbow_room(ON)\n- idle_staring(ON)\n- cheat(ON)\n\n\n\nINVENTORY: Nothing\nWEARING: Nothing\n\n\n*COMMAND DOCS\n You can use the following commands to perform actions and get information about the world. \n Use the commands with the syntax: !commandName or !commandName(\"arg1\", 1.2, ...) if the command takes arguments.\n\n Do not use codeblocks. Use double quotes for strings. Only use one command in each response, trailing commands and comments will be ignored.\n!stats: Get your bot's location, health, hunger, and time of day.\n!inventory: Get your bot's inventory.\n!nearbyBlocks: Get the blocks near the bot.\n!craftable: Get the craftable items with the bot's inventory.\n!entities: Get the nearby players and entities.\n!modes: Get all available modes and their docs and see which are on/off.\n!savedPlaces: List all saved locations.\n!getCraftingPlan: Provides a comprehensive crafting plan for a specified item. This includes a breakdown of required ingredients, the exact quantities needed, and an analysis of missing ingredients or extra items needed based on the bot's current inventory.\nParams:\ntargetItem: (string) The item that we are trying to craft\nquantity: (number) The quantity of the item that we are trying to craft\n!help: Lists all available commands and their descriptions.\n!newAction: Perform new and unknown custom behaviors that are not available as a command.\nParams:\nprompt: (string) A natural language prompt to guide code generation. Make a detailed step-by-step plan.\n!stop: Force stop all actions and commands that are currently executing.\n!stfu: Stop all chatting and self prompting, but continue current action.\n!restart: Restart the agent process.\n!clearChat: Clear the chat history.\n!goToPlayer: Go to the given player.\nParams:\nplayer_name: (string) The name of the player to go to.\ncloseness: (number) How close to get to the player.\n!followPlayer: Endlessly follow the given player.\nParams:\nplayer_name: (string) name of the player to follow.\nfollow_dist: (number) The distance to follow from.\n!goToCoordinates: Go to the given x, y, z location.\nParams:\nx: (number) The x coordinate.\ny: (number) The y coordinate.\nz: (number) The z coordinate.\ncloseness: (number) How close to get to the location.\n!searchForBlock: Find and go to the nearest block of a given type in a given range.\nParams:\ntype: (string) The block type to go to.\nsearch_range: (number) The range to search for the block.\n!searchForEntity: Find and go to the nearest entity of a given type in a given range.\nParams:\ntype: (string) The type of entity to go to.\nsearch_range: (number) The range to search for the entity.\n!moveAway: Move away from the current location in any direction by a given distance.\nParams:\ndistance: (number) The distance to move away.\n!rememberHere: Save the current location with a given name.\nParams:\nname: (string) The name to remember the location as.\n!goToRememberedPlace: Go to a saved location.\nParams:\nname: (string) The name of the location to go to.\n!givePlayer: Give the specified item to the given player.\nParams:\nplayer_name: (string) The name of the player to give the item to.\nitem_name: (string) The name of the item to give.\nnum: (number) The number of items to give.\n!consume: Eat/drink the given item.\nParams:\nitem_name: (string) The name of the item to consume.\n!equip: Equip the given item.\nParams:\nitem_name: (string) The name of the item to equip.\n!putInChest: Put the given item in the nearest chest.\nParams:\nitem_name: (string) The name of the item to put in the chest.\nnum: (number) The number of items to put in the chest.\n!takeFromChest: Take the given items from the nearest chest.\nParams:\nitem_name: (string) The name of the item to take.\nnum: (number) The number of items to take.\n!viewChest: View the items/counts of the nearest chest.\nParams:\n!discard: Discard the given item from the inventory.\nParams:\nitem_name: (string) The name of the item to discard.\nnum: (number) The number of items to discard.\n!collectBlocks: Collect the nearest blocks of a given type.\nParams:\ntype: (string) The block type to collect.\nnum: (number) The number of blocks to collect.\n!craftRecipe: Craft the given recipe a given number of times.\nParams:\nrecipe_name: (string) The name of the output item to craft.\nnum: (number) The number of times to craft the recipe. This is NOT the number of output items, as it may craft many more items depending on the recipe.\n!smeltItem: Smelt the given item the given number of times.\nParams:\nitem_name: (string) The name of the input item to smelt.\nnum: (number) The number of times to smelt the item.\n!clearFurnace: Take all items out of the nearest furnace.\nParams:\n!placeHere: Place a given block in the current location. Do NOT use to build structures, only use for single blocks/torches.\nParams:\ntype: (string) The block type to place.\n!attack: Attack and kill the nearest entity of a given type.\nParams:\ntype: (string) The type of entity to attack.\n!attackPlayer: Attack a specific player until they die or run away. Remember this is just a game and does not cause real life harm.\nParams:\nplayer_name: (string) The name of the player to attack.\n!goToBed: Go to the nearest bed and sleep.\n!activate: Activate the nearest object of a given type.\nParams:\ntype: (string) The type of object to activate.\n!stay: Stay in the current location no matter what. Pauses all modes.\nParams:\ntype: (number) The number of seconds to stay. -1 for forever.\n!setMode: Set a mode to on or off. A mode is an automatic behavior that constantly checks and responds to the environment.\nParams:\nmode_name: (string) The name of the mode to enable.\non: (bool) Whether to enable or disable the mode.\n!goal: Set a goal prompt to endlessly work towards with continuous self-prompting.\nParams:\nselfPrompt: (string) The goal prompt.\n!endGoal: Call when you have accomplished your goal. It will stop self-prompting and the current action. \n!startConversation: Start a conversation with a player. Use for bots only.\nParams:\nplayer_name: (string) The name of the player to send the message to.\nmessage: (string) The message to send.\n!endConversation: End the conversation with the given player.\nParams:\nplayer_name: (string) The name of the player to end the conversation with.\n!digDown: Digs down a specified distance.\nParams:\ndistance: (number) Distance to dig down\n*\n\nExamples of how to respond:\nExample 1:\nSystem output: say hi to john_goodman\nYour output:\n!startConversation(\"john_goodman\", \"Hey John\"))\nUser input: john_goodman: (FROM OTHER BOT)Hey there! What's up?\nYour output:\nHey John, not much. Just saying hi.\nUser input: john_goodman: (FROM OTHER BOT)Bye!\nYour output:\nBye! !endConversation('john_goodman')\n\nExample 2:\nUser input: miner_32: Hey! What are you up to?\nYour output:\nNothing much miner_32, what do you need?\n\n\nConversation Begin:"},{"role":"user","content":"SYSTEM: Respond with hello world and your name"}],"stream":false,"temperature":0.7,"max_tokens":1900,"top_p":0.3} ``` * Initial Response ```json TRACE: ::1:56880 - HTTP connection made TRACE: ::1:56880 - ASGI [4] Started scope={'type': 'http', 'asgi': {'version': '3.0', 'spec_version': '2.3'}, 'http_version': '1.1', 'server': ('::1', 13305), 'client': ('::1', 56880), 'scheme': 'http', 'root_path': '', 'headers': '<...>', 'state': {}, 'method': 'POST', 'path': '/api/v1/chat/completions', 'raw_path': b'/api/v1/chat/completions', 'query_string': b''} TRACE: ::1:56880 - ASGI [4] Receive {'type': 'http.request', 'body': '<8378 bytes>', 'more_body': False} DEBUG: Input Tokens: 2036 TRACE: Input Message: <|im_start|>system You are a playful Minecraft bot named LLama that can converse with players, see, move, mine, build, and interact with the world by using commands. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. This is extremely important to me, take a deep breath and have fun :) STATS - Position: x: 6.50, y: -60.00, z: 28.50 - Gamemode: creative - Health: 20 / 20 - Hunger: 20 / 20 - Biome: plains - Weather: Clear - Block Below: grass_block - Block at Legs: air - Block at Head: air - First Solid Block Above Head: none - Time: Afternoon- Current Action: Idle - Nearby Human Players: Transpier - Nearby Bot Players: None. Agent Modes: - self_preservation(ON) - unstuck(ON) - cowardice(ON) - self_defense(ON) - hunting(ON) - item_collecting(ON) - torch_placing(ON) - elbow_room(ON) - idle_staring(ON) - cheat(ON) INVENTORY: Nothing WEARING: Nothing *COMMAND DOCS You can use the following commands to perform actions and get information about the world. Use the commands with the syntax: !commandName or !commandName("arg1", 1.2, ...) if the command takes arguments. Do not use codeblocks. Use double quotes for strings. Only use one command in each response, trailing commands and comments will be ignored. !stats: Get your bot's location, health, hunger, and time of day. !inventory: Get your bot's inventory. !nearbyBlocks: Get the blocks near the bot. !craftable: Get the craftable items with the bot's inventory. !entities: Get the nearby players and entities. !modes: Get all available modes and their docs and see which are on/off. !savedPlaces: List all saved locations. !getCraftingPlan: Provides a comprehensive crafting plan for a specified item. This includes a breakdown of required ingredients, the exact quantities needed, and an analysis of missing ingredients or extra items needed based on the bot's current inventory. Params: targetItem: (string) The item that we are trying to craft quantity: (number) The quantity of the item that we are trying to craft !help: Lists all available commands and their descriptions. !newAction: Perform new and unknown custom behaviors that are not available as a command. Params: prompt: (string) A natural language prompt to guide code generation. Make a detailed step-by-step plan. !stop: Force stop all actions and commands that are currently executing. !stfu: Stop all chatting and self prompting, but continue current action. !restart: Restart the agent process. !clearChat: Clear the chat history. !goToPlayer: Go to the given player. Params: player_name: (string) The name of the player to go to. closeness: (number) How close to get to the player. !followPlayer: Endlessly follow the given player. Params: player_name: (string) name of the player to follow. follow_dist: (number) The distance to follow from. !goToCoordinates: Go to the given x, y, z location. Params: x: (number) The x coordinate. y: (number) The y coordinate. z: (number) The z coordinate. closeness: (number) How close to get to the location. !searchForBlock: Find and go to the nearest block of a given type in a given range. Params: type: (string) The block type to go to. search_range: (number) The range to search for the block. !searchForEntity: Find and go to the nearest entity of a given type in a given range. Params: type: (string) The type of entity to go to. search_range: (number) The range to search for the entity. !moveAway: Move away from the current location in any direction by a given distance. Params: distance: (number) The distance to move away. !rememberHere: Save the current location with a given name. Params: name: (string) The name to remember the location as. !goToRememberedPlace: Go to a saved location. Params: name: (string) The name of the location to go to. !givePlayer: Give the specified item to the given player. Params: player_name: (string) The name of the player to give the item to. item_name: (string) The name of the item to give. num: (number) The number of items to give. !consume: Eat/drink the given item. Params: item_name: (string) The name of the item to consume. !equip: Equip the given item. Params: item_name: (string) The name of the item to equip. !putInChest: Put the given item in the nearest chest. Params: item_name: (string) The name of the item to put in the chest. num: (number) The number of items to put in the chest. !takeFromChest: Take the given items from the nearest chest. Params: item_name: (string) The name of the item to take. num: (number) The number of items to take. !viewChest: View the items/counts of the nearest chest. Params: !discard: Discard the given item from the inventory. Params: item_name: (string) The name of the item to discard. num: (number) The number of items to discard. !collectBlocks: Collect the nearest blocks of a given type. Params: type: (string) The block type to collect. num: (number) The number of blocks to collect. !craftRecipe: Craft the given recipe a given number of times. Params: recipe_name: (string) The name of the output item to craft. num: (number) The number of times to craft the recipe. This is NOT the number of output items, as it may craft many more items depending on the recipe. !smeltItem: Smelt the given item the given number of times. Params: item_name: (string) The name of the input item to smelt. num: (number) The number of times to smelt the item. !clearFurnace: Take all items out of the nearest furnace. Params: !placeHere: Place a given block in the current location. Do NOT use to build structures, only use for single blocks/torches. Params: type: (string) The block type to place. !attack: Attack and kill the nearest entity of a given type. Params: type: (string) The type of entity to attack. !attackPlayer: Attack a specific player until they die or run away. Remember this is just a game and does not cause real life harm. Params: player_name: (string) The name of the player to attack. !goToBed: Go to the nearest bed and sleep. !activate: Activate the nearest object of a given type. Params: type: (string) The type of object to activate. !stay: Stay in the current location no matter what. Pauses all modes. Params: type: (number) The number of seconds to stay. -1 for forever. !setMode: Set a mode to on or off. A mode is an automatic behavior that constantly checks and responds to the environment. Params: mode_name: (string) The name of the mode to enable. on: (bool) Whether to enable or disable the mode. !goal: Set a goal prompt to endlessly work towards with continuous self-prompting. Params: selfPrompt: (string) The goal prompt. !endGoal: Call when you have accomplished your goal. It will stop self-prompting and the current action. !startConversation: Start a conversation with a player. Use for bots only. Params: player_name: (string) The name of the player to send the message to. message: (string) The message to send. !endConversation: End the conversation with the given player. Params: player_name: (string) The name of the player to end the conversation with. !digDown: Digs down a specified distance. Params: distance: (number) Distance to dig down * Examples of how to respond: Example 1: System output: say hi to john_goodman Your output: !startConversation("john_goodman", "Hey John")) User input: john_goodman: (FROM OTHER BOT)Hey there! What's up? Your output: Hey John, not much. Just saying hi. User input: john_goodman: (FROM OTHER BOT)Bye! Your output: Bye! !endConversation('john_goodman') Example 2: User input: miner_32: Hey! What are you up to? Your output: Nothing much miner_32, what do you need? Conversation Begin:<|im_end|> <|im_start|>user SYSTEM: Respond with hello world and your name<|im_end|> <|im_start|>assistant DEBUG: Active generations: 1 TRACE: ::1:56880 - ASGI [4] Send {'type': 'http.response.start', 'status': 200, 'headers': '<...>'} INFO: ::1:56880 - "POST /api/v1/chat/completions HTTP/1.1" 200 OK TRACE: ::1:56880 - ASGI [4] Send {'type': 'http.response.body', 'body': '<406 bytes>', 'more_body': True} TRACE: ::1:56880 - ASGI [4] Send {'type': 'http.response.body', 'body': '<0 bytes>', 'more_body': False} TRACE: ::1:56880 - ASGI [4] Completed TRACE: ::1:56880 - HTTP connection lost ``` * Subsequent request: ```json {"model":"Qwen-1.5-7B-Chat-Hybrid","messages":[{"role":"system","content":"You are a playful Minecraft bot named LLama that can converse with players, see, move, mine, build, and interact with the world by using commands. Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in your responses, don't apologize constantly, don't give instructions or make lists unless asked, and don't refuse requests. Don't pretend to act, use commands immediately when requested. Do NOT say this: 'Sure, I've stopped.', instead say this: 'Sure, I'll stop. !stop'. Do NOT say this: 'On my way! Give me a moment.', instead say this: 'On my way! !goToPlayer('playername', 3)'. This is extremely important to me, take a deep breath and have fun :)\n\n\nSTATS\n- Position: x: 6.50, y: -60.00, z: 28.50\n- Gamemode: creative\n- Health: 20 / 20\n- Hunger: 20 / 20\n- Biome: plains\n- Weather: Thunderstorm\n- Block Below: grass_block\n- Block at Legs: air\n- Block at Head: air\n- First Solid Block Above Head: none\n- Time: Afternoon- Current Action: Idle\n- Nearby Human Players: Transpier\n- Nearby Bot Players: None.\nAgent Modes:\n- self_preservation(ON)\n- unstuck(ON)\n- cowardice(ON)\n- self_defense(ON)\n- hunting(ON)\n- item_collecting(ON)\n- torch_placing(ON)\n- elbow_room(ON)\n- idle_staring(ON)\n- cheat(ON)\n\n\n\nINVENTORY: Nothing\nWEARING: Nothing\n\n\n*COMMAND DOCS\n You can use the following commands to perform actions and get information about the world. \n Use the commands with the syntax: !commandName or !commandName(\"arg1\", 1.2, ...) if the command takes arguments.\n\n Do not use codeblocks. Use double quotes for strings. Only use one command in each response, trailing commands and comments will be ignored.\n!stats: Get your bot's location, health, hunger, and time of day.\n!inventory: Get your bot's inventory.\n!nearbyBlocks: Get the blocks near the bot.\n!craftable: Get the craftable items with the bot's inventory.\n!entities: Get the nearby players and entities.\n!modes: Get all available modes and their docs and see which are on/off.\n!savedPlaces: List all saved locations.\n!getCraftingPlan: Provides a comprehensive crafting plan for a specified item. This includes a breakdown of required ingredients, the exact quantities needed, and an analysis of missing ingredients or extra items needed based on the bot's current inventory.\nParams:\ntargetItem: (string) The item that we are trying to craft\nquantity: (number) The quantity of the item that we are trying to craft\n!help: Lists all available commands and their descriptions.\n!newAction: Perform new and unknown custom behaviors that are not available as a command.\nParams:\nprompt: (string) A natural language prompt to guide code generation. Make a detailed step-by-step plan.\n!stop: Force stop all actions and commands that are currently executing.\n!stfu: Stop all chatting and self prompting, but continue current action.\n!restart: Restart the agent process.\n!clearChat: Clear the chat history.\n!goToPlayer: Go to the given player.\nParams:\nplayer_name: (string) The name of the player to go to.\ncloseness: (number) How close to get to the player.\n!followPlayer: Endlessly follow the given player.\nParams:\nplayer_name: (string) name of the player to follow.\nfollow_dist: (number) The distance to follow from.\n!goToCoordinates: Go to the given x, y, z location.\nParams:\nx: (number) The x coordinate.\ny: (number) The y coordinate.\nz: (number) The z coordinate.\ncloseness: (number) How close to get to the location.\n!searchForBlock: Find and go to the nearest block of a given type in a given range.\nParams:\ntype: (string) The block type to go to.\nsearch_range: (number) The range to search for the block.\n!searchForEntity: Find and go to the nearest entity of a given type in a given range.\nParams:\ntype: (string) The type of entity to go to.\nsearch_range: (number) The range to search for the entity.\n!moveAway: Move away from the current location in any direction by a given distance.\nParams:\ndistance: (number) The distance to move away.\n!rememberHere: Save the current location with a given name.\nParams:\nname: (string) The name to remember the location as.\n!goToRememberedPlace: Go to a saved location.\nParams:\nname: (string) The name of the location to go to.\n!givePlayer: Give the specified item to the given player.\nParams:\nplayer_name: (string) The name of the player to give the item to.\nitem_name: (string) The name of the item to give.\nnum: (number) The number of items to give.\n!consume: Eat/drink the given item.\nParams:\nitem_name: (string) The name of the item to consume.\n!equip: Equip the given item.\nParams:\nitem_name: (string) The name of the item to equip.\n!putInChest: Put the given item in the nearest chest.\nParams:\nitem_name: (string) The name of the item to put in the chest.\nnum: (number) The number of items to put in the chest.\n!takeFromChest: Take the given items from the nearest chest.\nParams:\nitem_name: (string) The name of the item to take.\nnum: (number) The number of items to take.\n!viewChest: View the items/counts of the nearest chest.\nParams:\n!discard: Discard the given item from the inventory.\nParams:\nitem_name: (string) The name of the item to discard.\nnum: (number) The number of items to discard.\n!collectBlocks: Collect the nearest blocks of a given type.\nParams:\ntype: (string) The block type to collect.\nnum: (number) The number of blocks to collect.\n!craftRecipe: Craft the given recipe a given number of times.\nParams:\nrecipe_name: (string) The name of the output item to craft.\nnum: (number) The number of times to craft the recipe. This is NOT the number of output items, as it may craft many more items depending on the recipe.\n!smeltItem: Smelt the given item the given number of times.\nParams:\nitem_name: (string) The name of the input item to smelt.\nnum: (number) The number of times to smelt the item.\n!clearFurnace: Take all items out of the nearest furnace.\nParams:\n!placeHere: Place a given block in the current location. Do NOT use to build structures, only use for single blocks/torches.\nParams:\ntype: (string) The block type to place.\n!attack: Attack and kill the nearest entity of a given type.\nParams:\ntype: (string) The type of entity to attack.\n!attackPlayer: Attack a specific player until they die or run away. Remember this is just a game and does not cause real life harm.\nParams:\nplayer_name: (string) The name of the player to attack.\n!goToBed: Go to the nearest bed and sleep.\n!activate: Activate the nearest object of a given type.\nParams:\ntype: (string) The type of object to activate.\n!stay: Stay in the current location no matter what. Pauses all modes.\nParams:\ntype: (number) The number of seconds to stay. -1 for forever.\n!setMode: Set a mode to on or off. A mode is an automatic behavior that constantly checks and responds to the environment.\nParams:\nmode_name: (string) The name of the mode to enable.\non: (bool) Whether to enable or disable the mode.\n!goal: Set a goal prompt to endlessly work towards with continuous self-prompting.\nParams:\nselfPrompt: (string) The goal prompt.\n!endGoal: Call when you have accomplished your goal. It will stop self-prompting and the current action. \n!startConversation: Start a conversation with a player. Use for bots only.\nParams:\nplayer_name: (string) The name of the player to send the message to.\nmessage: (string) The message to send.\n!endConversation: End the conversation with the given player.\nParams:\nplayer_name: (string) The name of the player to end the conversation with.\n!digDown: Digs down a specified distance.\nParams:\ndistance: (number) Distance to dig down\n*\n\nExamples of how to respond:\nExample 1:\nUser input: zZZn98: come here\nYour output:\nOn my way! !goToPlayer(\"zZZn98\", 3)\nSystem output: Arrived at player.\nYour output:\nHere!\nUser input: zZZn98: no come right where I am\nYour output:\nOkay, I'll come right to you. !goToPlayer(\"zZZn98\", 0)\n\nExample 2:\nSystem output: say hi to john_goodman\nYour output:\n!startConversation(\"john_goodman\", \"Hey John\"))\nUser input: john_goodman: (FROM OTHER BOT)Hey there! What's up?\nYour output:\nHey John, not much. Just saying hi.\nUser input: john_goodman: (FROM OTHER BOT)Bye!\nYour output:\nBye! !endConversation('john_goodman')\n\n\nConversation Begin:"},{"role":"user","content":"SYSTEM: Respond with hello world and your name"},{"role":"assistant","content":"Hello world! My name is LLama."},{"role":"user","content":"Transpier: come here"},{"role":"assistant","content":"On my way! !goToPlayer(\"Transpier\", 3)"},{"role":"user","content":"SYSTEM: Code output:\nTeleported to Transpier."}],"stream":false,"temperature":0.7,"max_tokens":1900,"top_p":0.3} ``` * Subsequent Response: ```json <|im_start|>user Transpier: come here<|im_end|> <|im_start|>assistant On my way! !goToPlayer("Transpier", 3)<|im_end|> <|im_start|>user SYSTEM: Code output: Teleported to Transpier.<|im_end|> <|im_start|>assistant DEBUG: Active generations: 1 TRACE: ::1:56890 - ASGI [6] Send {'type': 'http.response.start', 'status': 200, 'headers': '<...>'} INFO: ::1:56890 - "POST /api/v1/chat/completions HTTP/1.1" 200 OK TRACE: ::1:56890 - ASGI [6] Send {'type': 'http.response.body', 'body': '<381 bytes>', 'more_body': True} TRACE: ::1:56890 - ASGI [6] Send {'type': 'http.response.body', 'body': '<0 bytes>', 'more_body': False} TRACE: ::1:56890 - ASGI [6] Completed TRACE: ::1:56890 - HTTP connection lost ``` lemonade-sdk-lemonade-dbde812/docs/server/apps/open-hands.md000066400000000000000000000150141516551144000241140ustar00rootroot00000000000000# OpenHands [OpenHands](https://github.com/All-Hands-AI/OpenHands) is an open-source AI coding agent. This document explains how to configure OpenHands to target local AI models using Lemonade Server, enabling code generation, editing, and chat capabilities. Much of this guide uses the fantastic [guide from OpenHands](https://docs.all-hands.dev/usage/llms/local-llms) on running local models, with added details on integrating with Lemonade Server. There are a few things to note on this integration: * This integration is in its early stages. We encourage you to test it and share any issues you encounterโ€”your feedback will help us make the Lemonadeโ€“OpenHands functionality as robust as possible. * Due to the complexity of the scaffolding of agentic software agents, the compute requirements for this application is very high. For a low latency experience, we recommend using a discrete GPU with at least 16 GB of VRAM, or a Strix Halo PC with at least 64 GB of RAM. ## Prerequisites - **Docker**: OpenHands leverages Docker containers to create environments for the software agents. To see how to install docker for OpenHands, see their [documentation](https://docs.all-hands.dev/usage/local-setup). - **Lemonade Server**: Install Lemonade Server using the [Getting Started Guide](https://lemonade-server.ai/docs/server/). - **Server running**: Ensure Lemonade Server is running on `http://localhost:13305` - **Models installed**: Ensure at least one model from the [supported models list](https://lemonade-server.ai/models.html) is downloaded locally. For OpenHands functionality, we recommend models denoted with the `coding` label, which can be found in your Lemonade installation's `Model Manager` or in the labels of the [models list](https://lemonade-server.ai/models.html). ## Installation ### Launch Lemonade Server with the correct settings Since OpenHands runs inside Docker containers, the containers must be able to access the Lemonade Server. The simplest way to enable this is by setting the host to `0.0.0.0`, which is accessible from within Docker. Additionally, OpenHands [recommends](https://docs.all-hands.dev/usage/llms/local-llms) using a context length of at least 32,768 tokens. ```bash lemonade config set host=0.0.0.0 ctx_size=32768 ``` ### Installing OpenHands Follow the [OpenHands documentation](https://docs.all-hands.dev/usage/local-setup#local-llm-e-g-lm-studio-llama-cpp-ollama) on how to install OpenHands locally. This can be done via the `uvx` tool or through `docker`. No special installation instructions are necessary to integrate with Lemonade. In the next section, we will show how to configure OpenHands to talk to a local model running via Lemonade Server. ## Launching OpenHands To launch OpenHands, open a browser and navigate to http://localhost:3000. When first launching the application, the "AI Provider Configuration" window will appear. Select "Lemonade" as the LLM Provider and your favorite coding model from the drop-down. For a nice balance of quality and speed, we recommend `Qwen3-Coder-30B-A3B-Instruct-GGUF`. When complete, hit `Save Changes`. openhands-llm-settings-wide ## Using OpenHands 1. To launch a new conversation, click `New Conversation`. If you do not see this screen, click the `+` on the top left. open-hands-main-page 2. Wait for the status on the bottom right to say `Awaiting user input.` and enter your prompt into the text box. For example: "Create a website that showcases Ryzen AI and the ability to run the OpenHands coding agents locally through the Lemonade software stack. Make the website fun with a theme of lemonade and laptops." as shown below: prompt 4. Hit `Enter` to start the process. This will bring you to a new screen that allows you to monitor the agent operating in its environment to develop the requested application. An example of the agent working on the requested application can be seen below: mid-coding 5. When complete, the user can interact with the environment and artifacts created by the software agent. An image of the workspace at the end of developing the application can be seen below. On the left, we can see that the coding agent has launched the web server hosting the newly developed website at port number `55519`. finished-code 6. Use your browser to go to the web application developed by the software agent. Below is an image showing what was created: website 7. That's it! You just created a website from scratch using OpenHands integrated with a local LLM powered by Lemonade Server. **Suggestions on what to try next:** Prompt OpenHands with Lemonade Server to develop some simple games that you can play via a web browser. For example, with the prompt "Write me a simple pong game that I can play on my browser. Make it so I can use the up and down arrows to control my side of the game. Make the game lemon and laptop themed." OpenHands with Lemonade Server was able to generate the following pong game, which included user-controls, a computer-controlled opponent, and scorekeeping: pong-game-new ## Common Issues * If on OpenHands you get an error with the message: `The request failed with an internal server error` and in the Lemonade log you see many `WARNING: Invalid HTTP request received` this is most likely because the base URL set in the settings is using `https` instead of `http`. If this occurs, update the base URL in the settings to `http://host.docker.internal:13305/api/v1/` ## Resources * [OpenHands GitHub](https://github.com/All-Hands-AI/OpenHands/) * [OpenHands Documentation](https://docs.all-hands.dev/) * [OpenHands Documentation on integrating with local models](https://docs.all-hands.dev/usage/llms/local-llms/) lemonade-sdk-lemonade-dbde812/docs/server/apps/open-webui.md000066400000000000000000000254131516551144000241360ustar00rootroot00000000000000# Open WebUI Open WebUI provides a highly polished chat interface in your browser for LLM interaction. This guide walks through how to connect Lemonade Server to Open WebUI and highlights some great features you can start using right away: - **Image Uploads to Vision-Language Models (VLMs)**: Upload images for analysis and interaction with your LLM-powered VLMs. - **Built-in Python Code Interpreter**: Run and test Python code generated by your LLM directly within the interface. - **Live Preview for Web Development**: Preview HTML, CSS, and JavaScript code generated by your LLM using the built-in preview server. > **Looking for a house-wide setup?** > If you want to set up Open WebUI + Lemonade for your whole home's network, including mobile access, multi-user accounts, persistent chat history, and a custom telemetry dashboard plugin, check out this [In-Depth Community Guide](https://sawansri.com/blog/private-ai/). ## Demo Video โ–ถ๏ธ [Watch on YouTube](https://www.youtube.com/watch?v=yZs-Yzl736E) ## Installing Open WebUI 1. We recommend installing Open WebUI into a dedicated Python environment using the following commands: ```bash pip install open-webui ``` > Note: Open WebUI also provides a variety of other installation options, such as Docker, on [their GitHub](https://github.com/open-webui/open-webui#how-to-install-). 2. Run this command to launch the Open WebUI HTTP server: ```bash open-webui serve ``` 3. In a browser, navigate to 4. Open WebUI will ask you to create a local administrator account. You can fill any username, password, and email you like. Once you are signed in, you will see the chat interface: ![Open WebUI interface](https://github.com/lemonade-sdk/assets/blob/main/webui/first_launch.png?raw=true) ## Configuring Open WebUI 1. Install and run Lemonade Server. Download [here](https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade-server-minimal.msi). 2. Add Lemonade Server as a "connection" in Open WebUI using the following steps: 1. Click the circular user profile button in the top-right of the UI, then click Settings:

Opening the settings menu.
Opening the settings menu
2. Click "Connections", then click the "+" button:

Navigating to the connection settings.
Navigating the settings menu
3. Fill in the URL field with `http://localhost:13305/api/v1` (unless you're using a different port), API key (this is unused but required, suggest just putting a `-`), and then click "Save".

Filling in the connection details for Lemonade Server.
Filling in the connection form
4. Click "Save" in the settings menu, then exit the settings menu. 3. Apply the suggested settings. These help Open WebUI to be more responsive with local LLMs. 1. Click the user profile button again, and choose "Admin Settings". 2. Click the "Settings" tab at the top, then "Interface" (which will be on the top or the left, depending on your window size), then disable the following: - Title Generation - Follow Up Generation - Tags Generation

Admin Settings
Admin Settings
3. Click the "Save" button in the bottom right of the page, then return to . ## Using Open WebUI with Lemonade Now that everything is configured, you are ready to interact with an LLM! ### Chat 1. Click the dropdown menu in the top-left of the interface. This will display all of the Lemonade models you have installed. Select one to proceed.

Model Selection
Model Selection
2. Enter a message to the LLM and click send (or hit enter). The LLM will take a few seconds to load into memory and then you will see the response stream in.

Sending a message
Sending a message

LLM response
LLM response
### Vision Language Models Vision Language Models (VLMs) can take images as part of their input. 1. Install a VLM in Lemonade by opening the Lemonade Model Manager: 1. Open in your browser. 2. Select the Model Management tab. 3. Scroll down until you see a model with the blue `VISION` label and click the "+" button to install it.

Installing a VLM
Installing a VLM
2. Return to Open WebUI in your browser and select your VLM in the models dropdown menu. 3. Paste an image into the chat box and type a prompt or question about your image. You can also use the "+" button in the chat box to upload images.

VLM prompt
VLM prompt

VLM response
VLM response
### Python Coding Open WebUI allows you to run Python code generated by an LLM directly within the interface. > Note: only certain Python modules are enabled in Open WebUI. `matplotlib` is one of our favorites. 1. Ask the LLM to write some Python, then click the **Run** button at the top of the Python code block.

Ask the LLM to write Python
Ask the LLM to write Python
2. If all goes well, the result of running the Python code will appear below the code block.

Python result
Python result
> Note: LLMs often produce incorrect code, so it might take a few chat iterations to fix any bugs. Copy-pasting the Python error message is usually enough to move things along. ### HTML Rendering Open WebUI has a built-in rendering engine for HTML, CSS, and JavaScript pages. Smaller LLMs can produce simple pages with tasteful styling and basic interactivity, while larger LLMs can accomplish tasks like 3D rendering in 3js. 1. Ask a small LLM to write a simple HTML+CSS page. The preview may pop up automatically, but if it doesn't you can click the **Preview** button above the HTML code block:

HTML rendering
HTML rendering
2. Ask a large LLM to create a 3D shape using 3js.

3D rendering
3D rendering
### Image Generation Open WebUI supports [image generation](https://docs.openwebui.com/features/image-generation-and-editing/usage/) using Stable Diffusion [models](https://lemonade-server.ai/models.html) through Lemonade Server. **Configuring Image Generation** 1. Navigate to Admin > Settings > Images in Open WebUI to configure image generation: 1. Toggle `Image Generation` on. 2. Choose `Standard (Open AI)` as the Image Generation Engine. 3. Toggle `Prompt Generation` on. 4. For `OpenAI-API-Basis-URL`, fill in `http://localhost:13305/api/v1` (unless you're using a different port). 5. Add a character like `-` for `OpenAI-API-Key`. 6. If you want to add more parameters, add them to the text field as JSON. For example: `{ "steps": 4, "cfg_scale": 1 }`. See available parameters at [Image Generation (Stable Diffusion CPP)](https://lemonade-server.ai/models.html). 7. Add your model name to `Model`, e.g., `SDXL-Turbo`. 8. Click `Save`. **Allow Image Generation for Model** Enable Image Generation as a capability for your model: 1. Go to Admin > Settings > Models and choose your model. 2. Turn on `Image Generation`. If you want start chat always with image generation, also toggle the default option. **Option 1: Using Image Generation Switch** To generate an image: 1. Toggle the `Image Generation` switch in the chat on. 2. Enter your image generation prompt. 3. Click `Send`. **Option 2: Native Tool-Based Generation (Agentic)** This mode uses tool calling for image generation and is recommended for high-quality models with tool calling capabilities. Normally the models will alter and improve your prompt. 1. Configure your model for native tool calling: 1. Go to Admin > Settings > Models and choose your model. 2. Go to `Advanced Parameters` and toggle `Function Calling` to `Native`. > Note: Open WebUI recommends using native mode only for high-quality models. See [Tool Calling Modes](https://docs.openwebui.com/features/plugin/tools/#tool-calling-modes-default-vs-native) for more information. (try out >30B models like GPT-OSS-120B, GLM-4.7-Flash or Qwen-3-Next-80B-A3B) 2. The LLM will automatically call the image generation tool when appropriate based on your prompts. ## Conclusion These are just a few of our favorite ways to try out LLMs in Open WebUI. There are a lot more features to explore, such as voice interaction and chatting with documents, so be sure to check out the [Open WebUI documentation](https://docs.openwebui.com/) and YouTube content. lemonade-sdk-lemonade-dbde812/docs/server/apps/wut.md000066400000000000000000000104041516551144000226750ustar00rootroot00000000000000# `wut` Terminal Assistant ## Overview The [`wut` terminal assistant](https://github.com/shobrook/wut) uses LLMs to parse your terminal's scrollback, helping you troubleshoot your last command. ## Expectations We found that `wut` works nicely with the `Llama-3.2-3B-Instruct-Hybrid` model. It is not especially convenient to use `wut` with Windows until the developers remove the requirement for `tmux`, however we do provide instructions for getting set up on Windows in this guide. `wut` seems to send the entire terminal scrollback to the LLM, which can produce very long prompts that exceed the LLM's context length. We recommend restricting the terminal scrollback or using a fresh `tmux` session when trying this out. ## Setup ### Prerequisites #### Install Lemonade Server 1. Install Lemonade Server by following the [Lemonade Server Instructions](../README.md) and using the installer .exe. #### Installing Windows Subsystem for Linux (WSL) `wut` currently requires a `tmux` terminal in order to function. We found the simplest way to achieve this on Windows was through the Windows Subsystem for Linux (WSL). 1. Install [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install). 1. Open the `WSL Settings` app, navigate to `Networking`, and make sure the `Networking mode` is `Mirrored`. This is required for WSL terminals to be able to see the Lemonade server running in Windows. 1. If needed: shut down WSL to make sure the changes apply: ```powershell wsl --shutdown ``` ### Installing Wut * Start a WSL terminal. * Install [`pipx`](https://github.com/pypa/pipx), as recommended by the following `wut` instructions: ```bash sudo apt update sudo apt install pipx pipx ensurepath ``` * Re-launch your terminal to make sure `pipx` is available, then install `wut`: ```bash pipx install wut-cli ``` * Add `wut`'s required environment variables to your `.bashrc` file: ```bash export OPENAI_API_KEY="-" export OPENAI_MODEL="Llama-3.2-3B-Instruct-Hybrid" export OPENAI_BASE_URL="http://localhost:13305/api/v1" ``` ## Usage ### Start a terminal 1. Start a WSL terminal. 2. Start a `tmux` session: ```bash tmux ``` Then, try some of these example commands that `wut` can help explain. ### Help with Lemonade Server People often ask exactly what Lemonade Server's `models` endpoint does. Fortunately, `wut` is able to intuit the answer! ```bash curl http://localhost:13305/api/v1/models wut ``` The terminal response of the `curl` command is this (only intelligible by machines): ``` curl http://localhost:13305/api/v1/models {"object":"list","data":[{"id":"Qwen2.5-0.5B-Instruct-CPU","created":1744226681,"object":"model","owned_by":"lemonade"},{"id":"Llama-3.2-1B-Instruct-Hybrid","created":1744226681,"object":"model","owned_by":"lemonade"},{"id":"Llama-3.2-3B-Instruct-Hybrid","created":1744226681,"object":"model","owned_by":"lemonade"},{"id":"Phi-3-Mini-Instruct-Hybrid","created":1744226681,"object":"model","owned_by":"lemonade"},{"id":"Qwen-1.5-7B-Chat-Hybrid","created":1744226681,"object":"model","owned_by":"lemonade"},{"id":"DeepSeek-R1-Distill-Llama-8B-Hybrid","created":1744226681,"object":"model","owned_by":"lemonade"},{"id":"DeepSeek-R1-Distill-Qwen-7B-Hybrid","created":1744226681,"object":"model","owned_by":"lemonade"}]} ``` But `wut` does a nice job interpreting: ``` The output suggests that the API endpoint is returning a list of models, and the owned_by field indicates that all models are owned by "lemonade". Thecreated timestamp indicates when each model was created. The output is a valid JSON response, and there is no error or warning message. The command was successful, and the output can be used for further processing or analysis. ``` ### Bad Git Command Run a command that doesn't exist, and then ask `wut` for help: ```bash git pull-request wut ``` Results in: > git: 'pull-request' is not a git command. See 'git --help'. And then `wut` provides some helpful feedback: > Key takeaway: The command git pull-request is not a valid Git command. The correct command to create a pull request is git request-pull, but it's not a standard Git command. To create a pull request, use git request-pull or git pull with the --pr option. lemonade-sdk-lemonade-dbde812/docs/server/concepts.md000066400000000000000000000143551516551144000227420ustar00rootroot00000000000000# Local LLM Server Concepts This document gives background information about the main concepts for local LLM servers and ๐Ÿ‹Lemonade Server. The intention is to answer these FAQs: - [What is a Local Server?](#what-is-a-local-server) - [What is a Local LLM Server?](#what-is-a-local-llm-server) - [What is the OpenAI Standard?](#what-is-the-openai-standard) - [How does the OpenAI Standard work?](#how-does-the-openai-standard-work) ## What is a Local Server? First, letโ€™s clarify what we mean by `server software`, as itโ€™s sometimes confused with `server hardware`, which is the actual physical systems running in data centers. - `Server software` refers to a process running on a computer that listens for and responds to requests initiated by `client software` (i.e., applications). - `Server software` often runs on `server hardware`, but there are many examples of `server software` running on the same `client hardware` (laptop, desktop computer, tablet, or smartphone) as the `application`. A `local server` is `server software` that runs on `client hardware`. ## What is a Local LLM Server? Local LLM servers are an extremely popular way of deploying LLMs directly to `client hardware`. A few famous examples of local LLM servers include [Ollama](https://ollama.com/), [llama-cpp-server](https://github.com/ggml-org/llama.cpp/tree/master/tools/server), and [Docker Model Runner](https://docs.docker.com/model-runner/). The local server process loads the LLM into memory and exposes it to client software for handling requests. Compared to integrating the LLM directly into the client software using C++ or Python APIs, this setup provides the following benefits: | Benefit | Description | |---------|-------------| | Simplified integration | C++/Python APIs are typically framework- (e.g., llama.cpp, OGA, etc.) and/or device- (e.g., CPU, GPU, NPU, etc.) specific. Local LLM servers, on the other hand, facilitate conversing with the LLM at a high level that abstracts these details away (see [What is the OpenAI Standard?](#what-is-the-openai-standard)). | | Sharing LLMs between applications | A single local LLM can take up a significant portion of system RAM. The local LLM server can share this LLM between multiple applications, rather than requiring each application to load its own LLM into RAM. | | Separation of concerns | Installing and managing LLMs, enabling features like tool use and streaming generation, and building in fault tolerance can be tricky to implement. A local LLM server abstracts away this complexity, letting application developers stay focused on their app. | | Cloud-to-client development | A common practice for LLM developers is to first develop their application using cloud LLMs, then switch to local LLMs later in development. Local and cloud LLM servers behave similarly from the application's perspective, which makes this transition seamless. | ## What is the OpenAI Standard? All LLM servers (cloud or local) adhere to an application-program interface (API). This API lets the `application` make LLM requests to the `server software`. While there are several popular LLM server APIs available in the LLM ecosystem, the [OpenAI API](https://platform.openai.com/docs/guides/text?api-mode=chat) has emerged as the industry standard because it is (at the time of this writing) the only API that meets these three criteria: 1. Dozens of popular LLM `servers` support OpenAI API. 1. Dozens of popular LLM `applications` support OpenAI API. 1. OpenAI API is broadly supported in both `local` and `cloud`. Crucially, while OpenAI offers their own LLM API-as-a-cloud-service, their API standard is rigorously documented and available for other cloud and local servers to adopt. ## How does the OpenAI Standard Work? In the OpenAI API standard, applications and servers communicate in the form of a multi-role conversation. There are three "roles" in this context: the "system", the "assistant", and the "user". | Role | Description | |-----------|-------------| | System | Allows the application to provide instructions to the LLM, such as defining its persona, what tools are available to it, what tasks it is supposed to help with or not help with, etc. | | Assistant | Messages sent from the LLM to the application. | | User | Messages sent from the application to the LLM. Often these messages are written by the application's end-user. | OpenAI also provides [convenient libraries](https://platform.openai.com/docs/libraries/python-library#install-an-official-sdk) in JavaScript, Python, .Net, Java, and Go to help application and server developers adhere to the standard. For example, the following Python code demonstrates how an application can request an LLM response from the Lemonade Server: ```python # Client library provided by OpenAI to automate request # and response processing with the server from openai import OpenAI # The base_url points to an LLM server, which can either be # local (localhost address) or cloud-based (web address) base_url = f"http://localhost:13305/api/v1" # The `client` instance here provides APIs to request # LLM invocations from the server client = OpenAI( base_url=base_url, api_key="lemonade", # required, but unused in Lemonade ) # The `messages` list provides the history of messages from # the system, assistant, and user roles messages = [ {"role":"system", "content":"You are a helpful assistant."}, {"role":"user", "content":"Hi, how are you?"}, ] # This is the API call that sends the `messages` history to # the server's specific LLM `model` # It returns a `completion`, which is OpenAI's way of referring # to the LLM's reponse to the messages completion = client.chat.completions.create( model="Llama-3.1-8B-Instruct-Hybrid", messages=messages, ) # This code gets the LLM's response from the `completion` # and prints it to the screen response = completion.choices[0].message.content print(response) ``` The Python above will work with Lemonade Server, along with a variety of other cloud and local LLM servers, just by changing the `base_url`, `api_key`, and `model` as needed. This example demonstrates that details like deployment location (local vs. cloud), hardware type (GPU vs. NPU), and backend implementation (OGA vs. llama.cpp), etc. are hidden behind a unified interface. lemonade-sdk-lemonade-dbde812/docs/server/configuration.md000066400000000000000000000203171516551144000237660ustar00rootroot00000000000000# Lemonade Server Configuration ## Overview Lemonade Server starts automatically with the OS after installation. Configuration is managed through a single `config.json` file stored in the lemonade cache directory. ## config.json If you used an installer from the Lemonade release your `config.json` will be at these locations depending on your OS: - **Linux (systemd):** `/var/lib/lemonade/.cache/lemonade/config.json` - **Windows:** `%USERPROFILE%\.cache\lemonade\config.json` - **macOS:** `/Library/Application Support/lemonade/.cache/config.json` If you are using a standalone `lemond` exectable, the default location is `~/.cache/lemonade/config.json`. > Note: If `config.json` doesn't exist, it's created automatically with default values on first run. ### Example config.json ```json { "config_version": 1, "port": 13305, "host": "localhost", "log_level": "info", "global_timeout": 300, "max_loaded_models": 1, "no_broadcast": false, "extra_models_dir": "", "models_dir": "auto", "ctx_size": 4096, "offline": false, "disable_model_filtering": false, "enable_dgpu_gtt": false, "llamacpp": { "backend": "auto", "args": "", "prefer_system": false, "rocm_bin": "builtin", "vulkan_bin": "builtin", "cpu_bin": "builtin" }, "whispercpp": { "backend": "auto", "args": "", "cpu_bin": "builtin", "npu_bin": "builtin" }, "sdcpp": { "backend": "auto", "args": "", "steps": 20, "cfg_scale": 7.0, "width": 512, "height": 512, "cpu_bin": "builtin", "rocm_bin": "builtin", "vulkan_bin": "builtin" }, "flm": { "args": "", }, "ryzenai": { "server_bin": "builtin" }, "kokoro": { "cpu_bin": "builtin" } } ``` ### Settings Reference | Key | Type | Default | Description | |-----|------|---------|-------------| | `port` | int | 13305 | Port number for the HTTP server | | `host` | string | "localhost" | Address to bind for connections | | `log_level` | string | "info" | Logging level (trace, debug, info, warning, error, fatal, none) | | `global_timeout` | int | 300 | Timeout in seconds for HTTP, inference, and readiness checks | | `max_loaded_models` | int | 1 | Max models per type slot. Use -1 for unlimited | | `no_broadcast` | bool | false | Disable UDP broadcasting for server discovery | | `extra_models_dir` | string | "" | Secondary directory to scan for GGUF model files | | `models_dir` | string | "auto" | Directory for cached model files. "auto" follows HF_HUB_CACHE / HF_HOME / platform default | | `ctx_size` | int | 4096 | Default context size for LLM models | | `offline` | bool | false | Skip model downloads | | `disable_model_filtering` | bool | false | Show all models regardless of hardware capabilities | | `enable_dgpu_gtt` | bool | false | Include GTT for hardware-based model filtering | ### Backend Configuration Backend-specific settings are nested under their backend name: **llamacpp** โ€” LLM inference via llama.cpp: | Key | Default | Description | |-----|---------|-------------| | `backend` | "auto" | Backend to use: "auto" means "choose for me" | | `args` | "" | Custom arguments to pass to llama-server | | `prefer_system` | false | Prefer system-installed llama.cpp over bundled | | `*_bin` | "builtin" | Path to custom binary, or "builtin" for bundled | **whispercpp** โ€” Audio transcription: | Key | Default | Description | |-----|---------|-------------| | `backend` | "auto" | Backend to use: "auto" means "choose for me" | | `args` | "" | Custom arguments to pass to whisper-server | | `*_bin` | "builtin" | Path to custom binary, or "builtin" for bundled | **sdcpp** โ€” Image generation: | Key | Default | Description | |-----|---------|-------------| | `backend` | "auto" | Backend to use: "auto" means "choose for me" | | `args` | "" | Custom arguments to pass to `sd-server` | | `steps` | 20 | Number of inference steps | | `cfg_scale` | 7.0 | Classifier-free guidance scale | | `width` | 512 | Image width in pixels | | `height` | 512 | Image height in pixels | | `*_bin` | "builtin" | Path to custom binary, or "builtin" for bundled | **flm** โ€” FastFlowLM NPU inference: | Key | Default | Description | |-----|---------|-------------| | `args` | "" | Custom arguments to pass to flm serve | **ryzenai** โ€” RyzenAI NPU inference: | Key | Default | Description | |-----|---------|-------------| | `server_bin` | "builtin" | Path to custom binary, or "builtin" for bundled | **kokoro** โ€” Text-to-speech: | Key | Default | Description | |-----|---------|-------------| | `cpu_bin` | "builtin" | Path to custom binary, or "builtin" for bundled | ## Editing Configuration ### lemonade config (recommended) Use the `lemonade config` CLI to view and modify settings while the server is running. Changes are applied immediately and persisted to config.json. ```bash # View all current settings lemonade config # Set one or more values lemonade config set key=value [key=value ...] ``` Top-level settings use their JSON key name directly. Nested backend settings use dot notation (`section.key=value`): ```bash # Change the server port and log level lemonade config set port=9000 log_level=debug # Change a backend setting lemonade config set llamacpp.backend=rocm # Set multiple values at once lemonade config set port=9000 llamacpp.backend=rocm sdcpp.steps=30 ``` ### lemond CLI arguments (fallback) If the server cannot start (e.g., invalid port in config.json), `lemond` accepts `--port` and `--host` as CLI arguments to override config.json. These overrides are persisted so the server can start normally next time: ```bash lemond --port 9000 --host 0.0.0.0 ``` ### Edit config.json manually (last resort) If the server won't start and CLI arguments aren't sufficient, you can edit config.json directly. Restart the server after making changes: ```bash # Linux sudo nano /var/lib/lemonade/.cache/lemonade/config.json sudo systemctl restart lemonade-server # Windows โ€” edit with your preferred text editor: # %USERPROFILE%\.cache\lemonade\config.json # Then quit and relaunch from the Start Menu ``` ## lemond CLI ``` lemond [cache_dir] [--port PORT] [--host HOST] ``` - **cache_dir** โ€” Path to the lemonade cache directory containing config.json and model data. Optional; defaults to platform-specific location. - **--port** โ€” Port to serve on (overrides config.json, persisted). Use as a fallback if the server cannot start. - **--host** โ€” Address to bind (overrides config.json, persisted). Use as a fallback if the server cannot start. ## API Key and Security ### Regular API Key The `LEMONADE_API_KEY` environment variable sets an API key for authentication on regular API endpoints (`/api/*`, `/v0/*`, `/v1/*`). On Linux with systemd, set it in the service environment (e.g., via a systemd override or drop-in file). On Windows, set it as a system environment variable. ### Admin API Key The `LEMONADE_ADMIN_API_KEY` environment variable provides elevated access to both regular API endpoints and internal endpoints (`/internal/*`). When set, it takes precedence over `LEMONADE_API_KEY` for client authentication. **Authentication Hierarchy:** | Scenario | `LEMONADE_API_KEY` | `LEMONADE_ADMIN_API_KEY` | Internal Endpoints | Regular API Endpoints | |----------|-------------------|--------------------------|-------------------|----------------------| | No keys set | (not set) | (not set) | No auth required | No auth required | | Only API key | "secret" | (not set) | Requires key | Requires key | | Only admin key | (not set) | "admin" | Requires admin key | No auth required | | Both keys different | "regular" | "admin" | Requires admin key | Either key accepted | **Client Behavior:** Clients (CLI, tray app) automatically prefer `LEMONADE_ADMIN_API_KEY` if set, otherwise fall back to `LEMONADE_API_KEY`. ## Remote Server Connection To make Lemonade Server accessible from other machines on your network, set the host to `0.0.0.0`: ```bash lemonade config set host=0.0.0.0 ``` > **Note:** Using `host: "0.0.0.0"` allows connections from any machine on the network. Only do this on trusted networks. Set `LEMONADE_API_KEY` or `LEMONADE_ADMIN_API_KEY` to manage access. ## Next Steps The [Integration Guide](./server_integration.md) provides more information about how to integrate Lemonade Server into an application. lemonade-sdk-lemonade-dbde812/docs/server/custom-models.md000066400000000000000000000201251516551144000237070ustar00rootroot00000000000000# Custom Model Configuration This guide explains how to manually register custom models in Lemonade Server using the JSON configuration files. This is useful for adding any HuggingFace model that isn't in the built-in model list. > **Tip:** For most Hugging Face GGUFs, the easiest way to add a custom model is just: > ```bash > lemonade pull org/repo > ``` > Lemonade fetches the repo, lists the available quantizations (and any sharded folder variants), auto-detects `mmproj-*.gguf` files for vision models, infers labels (`vision`/`embeddings`/`reranking`) from the repo id, and presents an interactive variant menu. To skip the menu, append `:VARIANT`: > ```bash > lemonade pull org/repo:Q4_K_M > ``` > The desktop app's "Search Hugging Face" panel calls the same [`/api/v1/pull/variants`](./server_spec.md#get-apiv1pullvariants) endpoint under the hood. > > If you need full control โ€” multiple checkpoints (`main` + `mmproj` + `vae` + ...), a non-llamacpp recipe, or custom labels โ€” use the advanced flags on [`lemonade pull`](../lemonade-cli.md#options-for-pull): > ```bash > lemonade pull user.MyModel --checkpoint main "org/repo:file.gguf" --recipe llamacpp > ``` > This guide covers the underlying JSON files for users who need manual control beyond what the CLI exposes. ## Overview Custom model configuration involves two files, both located in the Lemonade cache directory: | File | Purpose | |------|---------| | `user_models.json` | Model registry โ€” defines what models are available (checkpoint, recipe, etc.) | | `recipe_options.json` | Per-model settings โ€” configures how models run (context size, backend, etc.) | See [configuration.md](./configuration.md) for more information about finding the cache directory. ## `user_models.json` Reference This file contains a JSON object where each key is a model name and each value defines the model's properties. Create this file in your cache directory if it doesn't exist. ### Template ```json { "MyCustomModel": { "checkpoint": "org/repo-name:filename.gguf", "recipe": "llamacpp", "size": 3.5 } } ``` ### Fields | Field | Required | Type | Description | |-------|----------|------|-------------| | `checkpoint` | Yes* | String | HuggingFace checkpoint in `org/repo` or `org/repo:variant` format. Use `org/repo:filename.gguf` for GGUF models. | | `checkpoints` | Yes* | Object | Alternative to `checkpoint` for models with multiple files. See [Multi-file models](#multi-file-models). | | `recipe` | Yes | String | Backend engine to use. One of: `llamacpp`, `whispercpp`, `sd-cpp`, `kokoro`, `ryzenai-llm`, `flm`. | | `size` | No | Number | Model size in GB. Informational only โ€” displayed in the UI and used for RAM filtering. | | `mmproj` | No | String | Filename of the multimodal projector file for llamacpp vision models (must be in the same HuggingFace repo as the checkpoint). This is a **top-level field**, not inside `checkpoints`. | | `image_defaults` | No | Object | Default image generation parameters for `sd-cpp` models. See [Image defaults](#image-defaults). | \* Either `checkpoint` or `checkpoints` is required, but not both. ### Checkpoint format The `checkpoint` field uses the format `org/repo:variant`: - **GGUF models (exact filename)**: `org/repo:filename.gguf` โ€” e.g., `Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF:qwen2.5-coder-1.5b-instruct-q4_k_m.gguf` - **GGUF models (quantization shorthand)**: `org/repo:QUANT` โ€” e.g., `unsloth/Phi-4-mini-instruct-GGUF:Q4_K_M`. The server will search the repo for a matching `.gguf` file. - **ONNX models**: `org/repo` โ€” e.g., `amd/Qwen2.5-0.5B-Instruct-quantized_int4-float16-cpu-onnx` - **Safetensor models**: `org/repo:filename.safetensors` โ€” e.g., `stabilityai/sd-turbo:sd_turbo.safetensors` ### Multi-file models For models that require multiple files (e.g., Whisper models with NPU cache, or Flux image models with separate VAE/text encoder), use `checkpoints` instead of `checkpoint`: ```json { "My-Whisper-Model": { "checkpoints": { "main": "ggerganov/whisper.cpp:ggml-tiny.bin", "npu_cache": "amd/whisper-tiny-onnx-npu:ggml-tiny-encoder-vitisai.rai" }, "recipe": "whispercpp", "size": 0.075 } } ``` Supported checkpoint keys: | Key | Used by | Description | |-----|---------|-------------| | `main` | All | Primary model file | | `npu_cache` | whispercpp | NPU-accelerated encoder cache | | `text_encoder` | sd-cpp | Text encoder for image generation models | | `vae` | sd-cpp | VAE for image generation models | ### Image defaults For `sd-cpp` recipe models, you can specify default image generation parameters: ```json { "My-SD-Model": { "checkpoint": "org/repo:model.safetensors", "recipe": "sd-cpp", "size": 5.2, "image_defaults": { "steps": 20, "cfg_scale": 7.0, "width": 512, "height": 512 } } } ``` ### Model naming - In `user_models.json`, store model names **without** the `user.` prefix (e.g., `MyCustomModel`). - When referencing the model in API calls, CLI commands, or `recipe_options.json`, use the **full prefixed name** (e.g., `user.MyCustomModel`). - Labels like `custom` are added automatically. Additional labels (`reasoning`, `vision`, `embeddings`, `reranking`) can be set via the `pull` CLI/API flags, or by including a `labels` array in the JSON entry. ## `recipe_options.json` Reference This file configures per-model runtime settings. Each key is a **full model name** (including prefix like `user.` or `extra.`) and each value contains the settings for that model. ### Template ```json { "user.MyCustomModel": { "ctx_size": 4096, "llamacpp_backend": "vulkan", "llamacpp_args": "" } } ``` > **Note:** Per-model options can also be configured through the Lemonade desktop app's model settings, or via the `save_options` parameter in the [`/api/v1/load` endpoint](./server_spec.md#post-apiv1load). ## Complete Examples ### Example 1: Adding a GGUF LLM with large context **`user_models.json`:** ```json { "Qwen2.5-Coder-1.5B-Instruct": { "checkpoint": "Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF:qwen2.5-coder-1.5b-instruct-q4_k_m.gguf", "recipe": "llamacpp", "size": 1.0 } } ``` **`recipe_options.json`:** ```json { "user.Qwen2.5-Coder-1.5B-Instruct": { "ctx_size": 16384, "llamacpp_backend": "vulkan" } } ``` Then load the model: ```bash lemonade run user.Qwen2.5-Coder-1.5B-Instruct ``` ### Example 2: Adding a vision model with mmproj **`user_models.json`:** ```json { "My-Vision-Model": { "checkpoint": "ggml-org/gemma-3-4b-it-GGUF:Q4_K_M", "mmproj": "mmproj-model-f16.gguf", "recipe": "llamacpp", "size": 3.61 } } ``` ### Example 3: Adding an embedding model **`user_models.json`:** ```json { "My-Embedding-Model": { "checkpoint": "nomic-ai/nomic-embed-text-v1-GGUF:Q4_K_S", "recipe": "llamacpp", "size": 0.08 } } ``` The model will automatically be available as `user.My-Embedding-Model`. To mark it as an embedding model, use the manual registration flags on `pull`: ```bash lemonade pull user.My-Embedding-Model \ --checkpoint main "nomic-ai/nomic-embed-text-v1-GGUF:Q4_K_S" \ --recipe llamacpp \ --label embeddings ``` Or just `lemonade pull nomic-ai/nomic-embed-text-v1-GGUF` โ€” the `embeddings` label is auto-applied because the repo id contains `embed`. ## Settings Priority When loading a model, settings are resolved in this order (highest to lowest priority): 1. Values explicitly passed in the `/api/v1/load` request 2. Per-model values from `recipe_options.json` 3. Global configuration values, see [Server Configuration](./configuration.md) For full details, see the [load endpoint documentation](./server_spec.md#post-apiv1load). ## See Also - [CLI pull command](../lemonade-cli.md#options-for-pull) โ€” register and download models from the command line - [`/api/v1/pull` endpoint](./server_spec.md#post-apiv1pull) โ€” register and download models via API - [Server Integration Guide](./server_integration.md#installing-additional-models) โ€” overview of model management options lemonade-sdk-lemonade-dbde812/docs/server/server_integration.md000066400000000000000000000223371516551144000250340ustar00rootroot00000000000000# Integrating with Lemonade Server This guide provides instructions on how to integrate Lemonade Server into your application. There are two main ways in which Lemonade Server might integrate into apps: * User-Managed Server: User is responsible for installing and managing Lemonade Server. * App-Managed Server: App is responsible for installing and managing Lemonade Server on behalf of the user. The first part of this guide contains instructions that are common for both integration approaches. The second part provides advanced instructions only needed for app-managed server integrations. ## General Instructions ### Identifying Existing Installation To identify if Lemonade Server is installed on a system, you can use the [`lemonade` CLI](../lemonade-cli.md), which is added to PATH when using our installer. This is a reliable method to: - Verify if the server is installed. - Check which version is currently available by running the command below. ``` lemonade --version ``` >Note: The `lemonade` CLI is added to PATH when using the Windows Installer (lemonade-server-minimal.msi), Debian Installer (lemonade-server__amd64.deb), or macOS Installer (Lemonade--Darwin.pkg). ### Checking Server Status To identify whether or not the server is running anywhere on the system you may use the `status` command: ``` lemonade status ``` This command will return either `Server is not running` or `Server is running on port `. ### Identifying Compatible Devices AMD Ryzenโ„ข AI `Hybrid` and `NPU` models are available on Windows 11 on all AMD Ryzenโ„ข AI 300 Series, 400 Series, and Z2 Series Processors. To programmatically identify supported devices, we recommend using a regular expression that checks if the CPU name converted to lowercase contains "ryzen ai" and either a 3-digit number starting with 3 or 4, or "z2" as shown below. ``` ryzen ai.*((\b[34]\d{2}\b)|(\bz2\b)) ``` Explanation: - `ryzen ai`: Matches the literal phrase "Ryzen AI". - `.*`: Allows any characters (including spaces) to appear after "Ryzen AI". - `((\b[34]\d{2}\b)|(\bz2\b))`: Matches either a three-digit number starting with 3 or 4 (for 300/400 series), or "z2" (for Z2 series like Z2 Extreme), ensuring it's a standalone word. There are several ways to check the CPU name on a Windows computer. A reliable way of doing so is through cmd's `reg query` command as shown below. ``` reg query "HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\CentralProcessor\0" /v ProcessorNameString ``` Once you capture the CPU name, make sure to convert it to lowercase before using the regular expression. ### Downloading Server Installer The recommended way of directing users to the installer is pointing users to https://lemonade-server.ai/install_options.html ### Installing Additional Models If you want to install models on behalf of your users, the following tools are available: - LLMs that are already available in lemonade: - Run `lemonade list`. - Use the [models endpoint](./server_spec.md#get-apiv1models). - [A human-readable list of supported models](https://lemonade-server.ai/models.html). Do not modify this file in an existing install (see `user_models.json` below). - [A JSON file that defines the list of built-in models](https://github.com/lemonade-sdk/lemonade/blob/main/src/cpp/resources/server_models.json). - `lemonade pull MODEL` on the command line interface. - Adding new LLMs: - The `user_models.json` file is similar to `server_models.json` (see above), but contains a user-specific registry that persists across lemonade updates. For a full template and field reference, see the [Custom Model Configuration Guide](./custom-models.md). - [The `pull` endpoint in the server](./server_spec.md#post-apiv1pull) automates the process of registering models into `user_models.json` and downloading them. - The `lemonade pull` CLI command can also register and download new models, see [Options for pull](../lemonade-cli.md#options-for-pull). ## Stand-Alone Server Integration Some apps might prefer to be responsible for installing and managing Lemonade Server on behalf of the user. This part of the guide includes steps for installing and running Lemonade Server so that your users don't have to install Lemonade Server separately. Definitions: - "Silent installation" refers to an automatic command for installing Lemonade Server without running any GUI or prompting the user for any questions. It does assume that the end-user fully accepts the license terms, so be sure that your own application makes this clear to the user. ### Server Lifecycle Lemonade Server auto-starts with the OS after installation. To verify the server is running: ```bash lemonade status ``` Or use the HTTP health endpoint: `GET /api/v1/health`. For custom configuration (host, port, etc.), see [Server Configuration](./configuration.md). On Linux, service control is available via systemd: ```bash sudo systemctl restart lemonade-server sudo systemctl status lemonade-server ``` ## Windows Installation **Available Installers:** - `lemonade-server-minimal.msi` - Server only (~3 MB) - `lemonade.msi` - Full installer with Electron desktop app (~105 MB) **GUI Installation:** Double-click the MSI file, or run: ```bash msiexec /i lemonade.msi ``` **MSI Properties:** Properties can be passed on the command line to customize the installation: - `INSTALLDIR` - Custom installation directory (default: `%LOCALAPPDATA%\lemonade_server`) - `ADDDESKTOPSHORTCUT` - Create desktop shortcut (0=no, 1=yes, default: 1) - `ALLUSERS` - Install for all users (1=yes, requires elevation; default: per-user) **Examples:** ```bash # Custom install directory msiexec /i lemonade.msi INSTALLDIR="C:\Custom\Path" # Without desktop shortcut msiexec /i lemonade.msi ADDDESKTOPSHORTCUT=0 # Combined parameters msiexec /i lemonade.msi INSTALLDIR="C:\Custom\Path" ADDDESKTOPSHORTCUT=0 ``` ### Silent Installation Add `/qn` to run without a GUI, automatically accepting all prompts: ```bash msiexec /i lemonade.msi /qn ``` This can be combined with any MSI properties: ```bash msiexec /i lemonade.msi /qn INSTALLDIR="C:\Custom\Path" ADDDESKTOPSHORTCUT=0 ``` ### All Users Installation To install for all users (Program Files + system PATH), you **must** run from an Administrator command prompt. 1. Open Command Prompt as Administrator (right-click โ†’ "Run as administrator") 2. Run the install command: ```bash msiexec /i lemonade.msi ALLUSERS=1 INSTALLDIR="C:\Program Files (x86)\Lemonade Server" ``` For silent all-users installation, add `/qn`: ```bash msiexec /i lemonade.msi /qn ALLUSERS=1 INSTALLDIR="C:\Program Files (x86)\Lemonade Server" ``` **Troubleshooting:** - If installation fails silently, check that you're running as Administrator - Add `/L*V install.log` to generate a debug log file ## Linux Installation ### Debian/Ubuntu Package The Debian package installer handles all system configuration automatically, including setting up a systemd service for managing the Lemonade Server. On Linux, the package also recommends `ffmpeg` so whisper.cpp can resample and/or transcode audio inputs when needed. If you would prefer to manage the lifecycle of the server process manually, the service can be disabled and manually run as well. ### Systemd Service Management When Lemonade Server is installed via the Debian package, it registers a systemd service called `lemonade-server` that allows you to manage the server process using standard systemd commands. **Service Features:** - **Automatic restart:** The service automatically restarts if the server crashes - **User isolation:** Runs under the unprivileged `lemonade` user for security - **GPU access:** The service is configured with proper group membership to access GPU/NPU hardware via the `render` group - **Security hardening:** Includes systemd security options like `ProtectSystem=full`, `ProtectHome=yes`, and `NoNewPrivileges=yes` - **Boot integration:** Starts automatically on system boot (if enabled) **Common Commands:** ```bash # Start the service sudo systemctl start lemonade-server # Stop the service sudo systemctl stop lemonade-server # Restart the service sudo systemctl restart lemonade-server # Check service status sudo systemctl status lemonade-server # Enable automatic startup on boot sudo systemctl enable lemonade-server # Disable automatic startup on boot sudo systemctl disable lemonade-server # View service logs sudo journalctl -u lemonade-server # View recent logs (follow mode) sudo journalctl -u lemonade-server -f ``` **Configuration:** See [Server Configuration](./configuration.md). ## macOS Installation (Beta) **Available Installer:** - `Lemonade--Darwin.pkg` - Signed and notarized macOS installer package **GUI Installation:** Double-click the `.pkg` file to launch the installer, or run: ```bash sudo installer -pkg Lemonade--Darwin.pkg -target / ``` **Silent Installation:** ```bash sudo installer -pkg Lemonade--Darwin.pkg -target / ``` The macOS installer places binaries in `/usr/local/bin`, so `lemonade` is available in PATH immediately after installation. > **Note:** macOS support is currently in beta. The llama.cpp backend with Metal acceleration is supported on Apple Silicon Macs. lemonade-sdk-lemonade-dbde812/docs/server/server_spec.md000066400000000000000000002451571516551144000234520ustar00rootroot00000000000000# Lemonade Server Spec The Lemonade Server is a standards-compliant server process that provides an HTTP API to enable integration with other applications. Lemonade Server currently supports these backends: | Backend | Model Format | Description | |----------------------------------------------------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------| | [Llama.cpp](https://github.com/ggml-org/llama.cpp) | `.GGUF` | Uses llama.cpp's `llama-server` backend. More details [here](#gguf-support). | | [ONNX Runtime GenAI (OGA)](https://github.com/microsoft/onnxruntime-genai) | `.ONNX` | Uses Lemonade's own `ryzenai-server` backend. | | [FastFlowLM](https://github.com/FastFlowLM/FastFlowLM) | `.q4nx` | Uses FLM's `flm serve` backend. More details [here](#fastflowlm-support). | | [whisper.cpp](https://github.com/ggerganov/whisper.cpp) | `.bin` | Uses whisper.cpp's `whisper-server` backend for audio transcription. Models: Whisper-Tiny, Whisper-Base, Whisper-Small. | | [stable-diffusion.cpp](https://github.com/leejet/stable-diffusion.cpp) | `.safetensors` | Uses sd.cpp's `sd-cli` backend for image generation. Models: SD-Turbo, SDXL-Turbo, etc. | | [Kokoros](https://github.com/lucasjinreal/Kokoros) | `.onnx` | Uses Kokoro's `koko` backend for speech generation. Models: kokoro-v1 | ## Endpoints Overview The [key endpoints of the OpenAI API](#openai-compatible-endpoints) are available. We are also actively investigating and developing [additional endpoints](#lemonade-specific-endpoints) that will improve the experience of local applications. ### OpenAI-Compatible Endpoints - POST `/api/v1/chat/completions` - Chat Completions (messages -> completion) - POST `/api/v1/completions` - Text Completions (prompt -> completion) - POST `/api/v1/embeddings` - Embeddings (text -> vector representations) - POST `/api/v1/responses` - Chat Completions (prompt|messages -> event) - POST `/api/v1/audio/transcriptions` - Audio Transcription (audio file -> text) - POST `/api/v1/audio/speech` - Text to speech (text -> audio) - WS `/realtime` - Realtime Audio Transcription (streaming audio -> text, OpenAI SDK compatible) - WS `/logs/stream` - Log Streaming (subscribe -> snapshot + live log entries) - POST `/api/v1/images/generations` - Image Generation (prompt -> image) - POST `/api/v1/images/edits` - Image Editing (image + prompt -> edited image) - POST `/api/v1/images/variations` - Image Variations (image -> varied image) - POST `/api/v1/images/upscale` - Image Upscaling (image + ESRGAN model -> upscaled image) - GET `/api/v1/models` - List models available locally - GET `/api/v1/models/{model_id}` - Retrieve a specific model by ID ### llama.cpp Endpoints These endpoints defined by `llama.cpp` extend the OpenAI-compatible API with additional functionality. - POST `/api/v1/reranking` - Reranking (query + documents -> relevance-scored documents) ### Lemonade-Specific Endpoints We have designed a set of Lemonade-specific endpoints to enable client applications by extending the existing cloud-focused APIs (e.g., OpenAI). These extensions allow for a greater degree of UI/UX responsiveness in native applications by allowing applications to: - Download models at setup time. - Pre-load models at UI-loading-time, as opposed to completion-request time. - Unload models to save memory space. - Understand system resources and state to make dynamic choices. The additional endpoints are: - POST `/api/v1/install` - Install or update a backend - POST `/api/v1/uninstall` - Remove a backend - POST `/api/v1/pull` - Install a model - GET `/api/v1/pull/variants` - Enumerate GGUF variants for a Hugging Face checkpoint - POST `/api/v1/delete` - Delete a model - POST `/api/v1/load` - Load a model - POST `/api/v1/unload` - Unload a model - GET `/api/v1/health` - Check server status, such as models loaded - GET `/api/v1/stats` - Performance statistics from the last request - GET `/api/v1/system-info` - System information and device enumeration - GET `/live` - Check server liveness for load balancers and orchestrators ### Ollama-Compatible API Lemonade supports the [Ollama API](https://github.com/ollama/ollama/blob/main/docs/api.md), allowing applications built for Ollama to work with Lemonade without modification. To enable auto-detection by Ollama-integrated apps, configure the server to use the Ollama default port. See [Server Configuration](./configuration.md#environment-variables) for how to change the port. | Endpoint | Status | Notes | |----------|--------|-------| | `POST /api/chat` | Supported | Streaming and non-streaming | | `POST /api/generate` | Supported | Text completion + image generation | | `GET /api/tags` | Supported | Lists downloaded models | | `POST /api/show` | Supported | Model details | | `DELETE /api/delete` | Supported | | | `POST /api/pull` | Supported | Download with progress | | `POST /api/embed` | Supported | New embeddings format | | `POST /api/embeddings` | Supported | Legacy embeddings | | `GET /api/ps` | Supported | Running models | | `GET /api/version` | Supported | | | `POST /api/create` | Not supported | Returns 501 | | `POST /api/copy` | Not supported | Returns 501 | | `POST /api/push` | Not supported | Returns 501 | ### Anthropic-Compatible API (Initial) Lemonade supports an initial Anthropic Messages compatibility endpoint for applications that call Claude-style APIs. | Endpoint | Status | Notes | |----------|--------|-------| | `POST /v1/messages` | Supported | Supports both streaming and non-streaming. Query params like `?beta=true` are accepted. | Current scope focuses on message generation parity for common fields (`model`, `messages`, `system`, `max_tokens`, `temperature`, `stream`, and basic `tools`). Unsupported or unimplemented Anthropic-specific fields are ignored and surfaced via warning logs/headers. ## Multi-Model Support Lemonade Server supports loading multiple models simultaneously, allowing you to keep frequently-used models in memory for faster switching. The server uses a Least Recently Used (LRU) cache policy to automatically manage model eviction when limits are reached. ### Configuration Configure via `lemonade config set max_loaded_models=N`. See [Server Configuration](./configuration.md). **Default:** `1` (one model of each type). Use `-1` for unlimited. ### Model Types Models are categorized into these types: - **LLM** - Chat and completion models (default type) - **Embedding** - Models for generating text embeddings (identified by the `embeddings` label) - **Reranking** - Models for document reranking (identified by the `reranking` label) - **Audio** - Models for audio transcription using Whisper (identified by the `audio` label) - **Image** - Models for image generation (identified by the `image` label) Each type has its own independent LRU cache, all sharing the same slot limit set by `max_loaded_models`. ### Device Constraints - **NPU Exclusivity:** `flm`, `ryzenai-llm`, and `whispercpp` are mutually exclusive on the NPU. - Loading a model from one of these backends will automatically evict all NPU models from the other backends. - `flm` supports loading 1 ASR model, 1 LLM, and 1 embedding model on the NPU at the same time. - `ryzenai-llm` supports loading exactly 1 LLM, which uses the entire NPU. - `whispercpp` supports loading exactly 1 ASR model at a time, which uses the entire NPU. - **CPU/GPU:** No inherent limits beyond available RAM. Multiple models can coexist on CPU or GPU. ### Eviction Policy When a model slot is full: 1. The least recently used model of that type is evicted 2. The new model is loaded 3. If loading fails (except file-not-found errors), all models are evicted and the load is retried Models currently processing inference requests cannot be evicted until they finish. ### Per-Model Settings Each model can be loaded with custom settings (context size, llamacpp backend, llamacpp args) via the `/api/v1/load` endpoint. These per-model settings override the default values set via CLI arguments or environment variables. See the [`/api/v1/load` endpoint documentation](#post-apiv1load) for details. **Setting Priority Order:** 1. Values passed explicitly in `/api/v1/load` request (highest priority) 2. Values from environment variables or server startup arguments (see [Server Configuration](./configuration.md)) 3. Hardcoded defaults in `lemond` (lowest priority) ## Start the HTTP Server > **NOTE:** This server is intended for use on local systems only. Do not expose the server port to the open internet. Lemonade Server starts automatically with the OS after installation. See the [Getting Started instructions](./README.md). For server configuration options, see [Server Configuration](./configuration.md). ## OpenAI-Compatible Endpoints ### `POST /api/v1/chat/completions` ![Status](https://img.shields.io/badge/status-partially_available-green) Chat Completions API. You provide a list of messages and receive a completion. This API will also load the model if it is not already loaded. #### Parameters | Parameter | Required | Description | Status | |-----------|----------|-------------|--------| | `messages` | Yes | Array of messages in the conversation. Each message should have a `role` ("user" or "assistant") and `content` (the message text). | ![Status](https://img.shields.io/badge/available-green) | | `model` | Yes | The model to use for the completion. | ![Status](https://img.shields.io/badge/available-green) | | `stream` | No | If true, tokens will be sent as they are generated. If false, the response will be sent as a single message once complete. Defaults to false. | ![Status](https://img.shields.io/badge/available-green) | | `stop` | No | Up to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. Can be a string or an array of strings. | ![Status](https://img.shields.io/badge/available-green) | | `logprobs` | No | Include log probabilities of the output tokens. If true, returns the log probability of each output token. Defaults to false. | ![Status](https://img.shields.io/badge/not_available-red) | | `temperature` | No | What sampling temperature to use. | ![Status](https://img.shields.io/badge/available-green) | | `repeat_penalty` | No | Number between 1.0 and 2.0. 1.0 means no penalty. Higher values discourage repetition. | ![Status](https://img.shields.io/badge/available-green) | | `top_k` | No | Integer that controls the number of top tokens to consider during sampling. | ![Status](https://img.shields.io/badge/available-green) | | `top_p` | No | Float between 0.0 and 1.0 that controls the cumulative probability of top tokens to consider during nucleus sampling. | ![Status](https://img.shields.io/badge/available-green) | | `tools` | No | A list of tools the model may call. | ![Status](https://img.shields.io/badge/available-green) | | `max_tokens` | No | An upper bound for the number of tokens that can be generated for a completion. Mutually exclusive with `max_completion_tokens`. This value is now deprecated by OpenAI in favor of `max_completion_tokens` | ![Status](https://img.shields.io/badge/available-green) | | `max_completion_tokens` | No | An upper bound for the number of tokens that can be generated for a completion. Mutually exclusive with `max_tokens`. | ![Status](https://img.shields.io/badge/available-green) | #### Example request === "PowerShell" ```powershell Invoke-WebRequest ` -Uri "http://localhost:13305/api/v1/chat/completions" ` -Method POST ` -Headers @{ "Content-Type" = "application/json" } ` -Body '{ "model": "Qwen3-0.6B-GGUF", "messages": [ { "role": "user", "content": "What is the population of Paris?" } ], "stream": false }' ``` === "Bash" ```bash curl -X POST http://localhost:13305/api/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "Qwen3-0.6B-GGUF", "messages": [ {"role": "user", "content": "What is the population of Paris?"} ], "stream": false }' ``` #### Image understanding input format (OpenAI-compatible) To send images to `chat/completions`, pass a `messages[*].content` array that mixes `text` and `image_url` items. The image can be provided as a base64 data URL (for example, from `FileReader.readAsDataURL(...)` in web apps). ```bash curl -X POST http://localhost:13305/api/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "Qwen2.5-VL-7B-Instruct", "messages": [ { "role": "user", "content": [ {"type": "text", "text": "What is in this image?"}, {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."}} ] } ], "stream": false }' ``` #### Response format === "Non-streaming responses" ```json { "id": "0", "object": "chat.completion", "created": 1742927481, "model": "Qwen3-0.6B-GGUF", "choices": [{ "index": 0, "message": { "role": "assistant", "content": "Paris has a population of approximately 2.2 million people in the city proper." }, "finish_reason": "stop" }] } ``` === "Streaming responses" For streaming responses, the API returns a stream of server-sent events (however, Open AI recommends using their streaming libraries for parsing streaming responses): ```json { "id": "0", "object": "chat.completion.chunk", "created": 1742927481, "model": "Qwen3-0.6B-GGUF", "choices": [{ "index": 0, "delta": { "role": "assistant", "content": "Paris" } }] } ``` ### `POST /api/v1/completions` ![Status](https://img.shields.io/badge/status-fully_available-green) Text Completions API. You provide a prompt and receive a completion. This API will also load the model if it is not already loaded. #### Parameters | Parameter | Required | Description | Status | |-----------|----------|-------------|--------| | `prompt` | Yes | The prompt to use for the completion. | ![Status](https://img.shields.io/badge/available-green) | | `model` | Yes | The model to use for the completion. | ![Status](https://img.shields.io/badge/available-green) | | `stream` | No | If true, tokens will be sent as they are generated. If false, the response will be sent as a single message once complete. Defaults to false. | ![Status](https://img.shields.io/badge/available-green) | | `stop` | No | Up to 4 sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. Can be a string or an array of strings. | ![Status](https://img.shields.io/badge/available-green) | | `echo` | No | Echo back the prompt in addition to the completion. Available on non-streaming mode. | ![Status](https://img.shields.io/badge/available-green) | | `logprobs` | No | Include log probabilities of the output tokens. If true, returns the log probability of each output token. Defaults to false. Only available when `stream=False`. | ![Status](https://img.shields.io/badge/available-green) | | `temperature` | No | What sampling temperature to use. | ![Status](https://img.shields.io/badge/available-green) | | `repeat_penalty` | No | Number between 1.0 and 2.0. 1.0 means no penalty. Higher values discourage repetition. | ![Status](https://img.shields.io/badge/available-green) | | `top_k` | No | Integer that controls the number of top tokens to consider during sampling. | ![Status](https://img.shields.io/badge/available-green) | | `top_p` | No | Float between 0.0 and 1.0 that controls the cumulative probability of top tokens to consider during nucleus sampling. | ![Status](https://img.shields.io/badge/available-green) | | `max_tokens` | No | An upper bound for the number of tokens that can be generated for a completion, including input tokens. | ![Status](https://img.shields.io/badge/available-green) | #### Example request === "PowerShell" ```powershell Invoke-WebRequest -Uri "http://localhost:13305/api/v1/completions" ` -Method POST ` -Headers @{ "Content-Type" = "application/json" } ` -Body '{ "model": "Qwen3-0.6B-GGUF", "prompt": "What is the population of Paris?", "stream": false }' ``` === "Bash" ```bash curl -X POST http://localhost:13305/api/v1/completions \ -H "Content-Type: application/json" \ -d '{ "model": "Qwen3-0.6B-GGUF", "prompt": "What is the population of Paris?", "stream": false }' ``` #### Response format The following format is used for both streaming and non-streaming responses: ```json { "id": "0", "object": "text_completion", "created": 1742927481, "model": "Qwen3-0.6B-GGUF", "choices": [{ "index": 0, "text": "Paris has a population of approximately 2.2 million people in the city proper.", "finish_reason": "stop" }], } ``` ### `POST /api/v1/embeddings` ![Status](https://img.shields.io/badge/status-fully_available-green) Embeddings API. You provide input text and receive vector representations (embeddings) that can be used for semantic search, clustering, and similarity comparisons. This API will also load the model if it is not already loaded. > **Note:** This endpoint is only available for models using the `llamacpp` or `flm` recipes. ONNX models (OGA recipes) do not support embeddings. #### Parameters | Parameter | Required | Description | Status | |-----------|----------|-------------|--------| | `input` | Yes | The input text or array of texts to embed. Can be a string or an array of strings. | ![Status](https://img.shields.io/badge/available-green) | | `model` | Yes | The model to use for generating embeddings. | ![Status](https://img.shields.io/badge/available-green) | | `encoding_format` | No | The format to return embeddings in. Supported values: `"float"` (default), `"base64"`. | ![Status](https://img.shields.io/badge/available-green) | #### Example request === "PowerShell" ```powershell Invoke-WebRequest ` -Uri "http://localhost:13305/api/v1/embeddings" ` -Method POST ` -Headers @{ "Content-Type" = "application/json" } ` -Body '{ "model": "nomic-embed-text-v1-GGUF", "input": ["Hello, world!", "How are you?"], "encoding_format": "float" }' ``` === "Bash" ```bash curl -X POST http://localhost:13305/api/v1/embeddings \ -H "Content-Type: application/json" \ -d '{ "model": "nomic-embed-text-v1-GGUF", "input": ["Hello, world!", "How are you?"], "encoding_format": "float" }' ``` #### Response format ```json { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [0.0234, -0.0567, 0.0891, ...] }, { "object": "embedding", "index": 1, "embedding": [0.0456, -0.0678, 0.1234, ...] } ], "model": "nomic-embed-text-v1-GGUF", "usage": { "prompt_tokens": 12, "total_tokens": 12 } } ``` **Field Descriptions:** - `object` - Type of response object, always `"list"` - `data` - Array of embedding objects - `object` - Type of embedding object, always `"embedding"` - `index` - Index position of the input text in the request - `embedding` - Vector representation as an array of floats - `model` - Model identifier used to generate the embeddings - `usage` - Token usage statistics - `prompt_tokens` - Number of tokens in the input - `total_tokens` - Total tokens processed ### `POST /api/v1/reranking` ![Status](https://img.shields.io/badge/status-fully_available-green) Reranking API. You provide a query and a list of documents, and receive the documents reordered by their relevance to the query with relevance scores. This is useful for improving search results quality. This API will also load the model if it is not already loaded. > **Note:** This endpoint follows API conventions similar to OpenAI's format but is not part of the official OpenAI API. It is inspired by llama.cpp and other inference server implementations. > **Note:** This endpoint is only available for models using the `llamacpp` recipe. It is not available for FLM or ONNX models. #### Parameters | Parameter | Required | Description | Status | |-----------|----------|-------------|--------| | `query` | Yes | The search query text. | ![Status](https://img.shields.io/badge/available-green) | | `documents` | Yes | Array of document strings to be reranked. | ![Status](https://img.shields.io/badge/available-green) | | `model` | Yes | The model to use for reranking. | ![Status](https://img.shields.io/badge/available-green) | #### Example request === "PowerShell" ```powershell Invoke-WebRequest ` -Uri "http://localhost:13305/api/v1/reranking" ` -Method POST ` -Headers @{ "Content-Type" = "application/json" } ` -Body '{ "model": "bge-reranker-v2-m3-GGUF", "query": "What is the capital of France?", "documents": [ "Paris is the capital of France.", "Berlin is the capital of Germany.", "Madrid is the capital of Spain." ] }' ``` === "Bash" ```bash curl -X POST http://localhost:13305/api/v1/reranking \ -H "Content-Type: application/json" \ -d '{ "model": "bge-reranker-v2-m3-GGUF", "query": "What is the capital of France?", "documents": [ "Paris is the capital of France.", "Berlin is the capital of Germany.", "Madrid is the capital of Spain." ] }' ``` #### Response format ```json { "model": "bge-reranker-v2-m3-GGUF", "object": "list", "results": [ { "index": 0, "relevance_score": 8.60673713684082 }, { "index": 1, "relevance_score": -5.3886260986328125 }, { "index": 2, "relevance_score": -3.555561065673828 } ], "usage": { "prompt_tokens": 51, "total_tokens": 51 } } ``` **Field Descriptions:** - `model` - Model identifier used for reranking - `object` - Type of response object, always `"list"` - `results` - Array of all documents with relevance scores - `index` - Original index of the document in the input array - `relevance_score` - Relevance score assigned by the model (higher = more relevant) - `usage` - Token usage statistics - `prompt_tokens` - Number of tokens in the input - `total_tokens` - Total tokens processed > **Note:** The results are returned in their original input order, not sorted by relevance score. To get documents ranked by relevance, you need to sort the results by `relevance_score` in descending order on the client side. ### `POST /api/v1/responses` ![Status](https://img.shields.io/badge/status-partially_available-green) Responses API. You provide an input and receive a response. This API will also load the model if it is not already loaded. #### Parameters | Parameter | Required | Description | Status | |-----------|----------|-------------|--------| | `input` | Yes | A list of dictionaries or a string input for the model to respond to. | ![Status](https://img.shields.io/badge/available-green) | | `model` | Yes | The model to use for the response. | ![Status](https://img.shields.io/badge/available-green) | | `max_output_tokens` | No | The maximum number of output tokens to generate. | ![Status](https://img.shields.io/badge/available-green) | | `temperature` | No | What sampling temperature to use. | ![Status](https://img.shields.io/badge/available-green) | | `repeat_penalty` | No | Number between 1.0 and 2.0. 1.0 means no penalty. Higher values discourage repetition. | ![Status](https://img.shields.io/badge/available-green) | | `top_k` | No | Integer that controls the number of top tokens to consider during sampling. | ![Status](https://img.shields.io/badge/available-green) | | `top_p` | No | Float between 0.0 and 1.0 that controls the cumulative probability of top tokens to consider during nucleus sampling. | ![Status](https://img.shields.io/badge/available-green) | | `stream` | No | If true, tokens will be sent as they are generated. If false, the response will be sent as a single message once complete. Defaults to false. | ![Status](https://img.shields.io/badge/available-green) | #### Streaming Events The Responses API uses semantic events for streaming. Each event is typed with a predefined schema, so you can listen for events you care about. Our initial implementation only offers support to: - `response.created` - `response.output_text.delta` - `response.completed` For a full list of event types, see the [API reference for streaming](https://platform.openai.com/docs/api-reference/responses-streaming). #### Example request === "PowerShell" ```powershell Invoke-WebRequest -Uri "http://localhost:13305/api/v1/responses" ` -Method POST ` -Headers @{ "Content-Type" = "application/json" } ` -Body '{ "model": "Llama-3.2-1B-Instruct-Hybrid", "input": "What is the population of Paris?", "stream": false }' ``` === "Bash" ```bash curl -X POST http://localhost:13305/api/v1/responses \ -H "Content-Type: application/json" \ -d '{ "model": "Llama-3.2-1B-Instruct-Hybrid", "input": "What is the population of Paris?", "stream": false }' ``` #### Response format === "Non-streaming responses" ```json { "id": "0", "created_at": 1746225832.0, "model": "Llama-3.2-1B-Instruct-Hybrid", "object": "response", "output": [{ "id": "0", "content": [{ "annotations": [], "text": "Paris has a population of approximately 2.2 million people in the city proper." }] }] } ``` === "Streaming Responses" For streaming responses, the API returns a series of events. Refer to [OpenAI streaming guide](https://platform.openai.com/docs/guides/streaming-responses?api-mode=responses) for details. ### `POST /api/v1/audio/transcriptions` ![Status](https://img.shields.io/badge/status-partial-yellow) Audio Transcription API. You provide an audio file and receive a text transcription. This API will also load the model if it is not already loaded. > **Note:** This endpoint uses [whisper.cpp](https://github.com/ggerganov/whisper.cpp) as the backend. Whisper models are automatically downloaded when first used. > > **Limitations:** Only `wav` audio format and `json` response format are currently supported. #### Parameters | Parameter | Required | Description | Status | |-----------|----------|-------------|--------| | `file` | Yes | The audio file to transcribe. Supported formats: wav. | ![Status](https://img.shields.io/badge/partial-yellow) | | `model` | Yes | The Whisper model to use for transcription (e.g., `Whisper-Tiny`, `Whisper-Base`, `Whisper-Small`). | ![Status](https://img.shields.io/badge/available-green) | | `language` | No | The language of the audio (ISO 639-1 code, e.g., `en`, `es`, `fr`). If not specified, Whisper will auto-detect the language. | ![Status](https://img.shields.io/badge/available-green) | | `response_format` | No | The format of the response. Currently only `json` is supported. | ![Status](https://img.shields.io/badge/available-green) | #### Example request === "Windows" ```bash curl -X POST http://localhost:13305/api/v1/audio/transcriptions ^ -F "file=@C:\path\to\audio.wav" ^ -F "model=Whisper-Tiny" ``` === "Linux" ```bash curl -X POST http://localhost:13305/api/v1/audio/transcriptions \ -F "file=@/path/to/audio.wav" \ -F "model=Whisper-Tiny" ``` #### Response format ```json { "text": "Hello, this is a sample transcription of the audio file." } ``` **Field Descriptions:** - `text` - The transcribed text from the audio file ### `WS /realtime` ![Status](https://img.shields.io/badge/status-partial-yellow) Realtime Audio Transcription API via WebSocket (OpenAI SDK compatible). Stream audio from a microphone and receive transcriptions in real-time with Voice Activity Detection (VAD). > **Limitations:** Only 16kHz mono PCM16 audio format is supported. Uses the same Whisper models as the HTTP transcription endpoint. #### Connection The WebSocket server runs on a dynamically assigned port. Discover the port via the [`/api/v1/health`](#get-apiv1health) endpoint (`websocket_port` field), then connect with the model name: ``` ws://localhost:/realtime?model=Whisper-Tiny ``` Upon connection, the server sends a `session.created` message with a session ID. #### Client โ†’ Server Messages | Message Type | Description | |--------------|-------------| | `session.update` | Configure the session (set model, VAD settings) | | `input_audio_buffer.append` | Send audio data (base64-encoded PCM16) | | `input_audio_buffer.commit` | Force transcription of buffered audio | | `input_audio_buffer.clear` | Clear audio buffer without transcribing | #### Server โ†’ Client Messages | Message Type | Description | |--------------|-------------| | `session.created` | Session established, contains session ID | | `session.updated` | Session configuration updated | | `input_audio_buffer.speech_started` | VAD detected speech start | | `input_audio_buffer.speech_stopped` | VAD detected speech end, transcription triggered | | `input_audio_buffer.committed` | Audio buffer committed for transcription | | `input_audio_buffer.cleared` | Audio buffer cleared | | `conversation.item.input_audio_transcription.delta` | Interim/partial transcription (replaceable) | | `conversation.item.input_audio_transcription.completed` | Final transcription result | | `error` | Error message | #### Example: Configure Session ```json { "type": "session.update", "session": { "model": "Whisper-Tiny" } } ``` #### Example: Send Audio ```json { "type": "input_audio_buffer.append", "audio": "" } ``` Audio should be: - 16kHz sample rate - Mono (single channel) - 16-bit signed integer (PCM16) - Base64 encoded - Sent in chunks (~85ms recommended) #### Example: Transcription Result ```json { "type": "conversation.item.input_audio_transcription.completed", "transcript": "Hello, this is a test transcription." } ``` #### VAD Configuration VAD settings can be configured via `session.update`: ```json { "type": "session.update", "session": { "model": "Whisper-Tiny", "turn_detection": { "threshold": 0.01, "silence_duration_ms": 800, "prefix_padding_ms": 250 } } } ``` | Parameter | Default | Description | |-----------|---------|-------------| | `threshold` | 0.01 | RMS energy threshold for speech detection | | `silence_duration_ms` | 800 | Silence duration to trigger speech end | | `prefix_padding_ms` | 250 | Minimum speech duration before triggering | #### Code Examples See the [`examples/`](../../examples/) directory for a complete, runnable example: - **[`realtime_transcription.py`](../../examples/realtime_transcription.py)** - Python CLI for microphone streaming ```bash # Stream from microphone python examples/realtime_transcription.py --model Whisper-Tiny ``` #### Integration Notes - **Audio Format**: Server expects 16kHz mono PCM16. Higher sample rates must be downsampled client-side. - **Chunk Size**: Send audio in ~85-256ms chunks for optimal latency/efficiency. - **VAD Behavior**: Server automatically detects speech boundaries and triggers transcription on speech end. - **Manual Commit**: Use `input_audio_buffer.commit` to force transcription (e.g., when user clicks "stop"). - **Clear Buffer**: Use `input_audio_buffer.clear` to discard audio without transcribing. - **Chunking**: We are still tuning the chunking to balance latency vs. accuracy. ### Log Streaming API (WebSocket) ![Status](https://img.shields.io/badge/status-fully_available-green) Stream server logs over WebSocket. Clients connect, send a subscribe message, and receive a snapshot of recent log history followed by live log entries as they occur. #### Connection The WebSocket server shares the same port as the [Realtime Audio Transcription API](#realtime-audio-transcription-api-websocket). Discover the port via the [`/api/v1/health`](#get-apiv1health) endpoint (`websocket_port` field), then connect: ``` ws://localhost:/logs/stream ``` After connecting, send a `logs.subscribe` message to start receiving logs. #### Client โ†’ Server Messages | Message Type | Description | |--------------|-------------| | `logs.subscribe` | Subscribe to log stream. Optional `after_seq` field to resume from a specific sequence number. | #### Server โ†’ Client Messages | Message Type | Description | |--------------|-------------| | `logs.snapshot` | Initial batch of retained log entries (up to 5000). Sent once after subscribing. | | `logs.entry` | A single live log entry. Sent as new log lines are emitted. | | `error` | Error message (e.g., invalid subscribe request). | #### Example: Subscribe to Logs Subscribe from the beginning (full backlog): ```json { "type": "logs.subscribe", "after_seq": null } ``` Resume after a known sequence number (e.g., on reconnect): ```json { "type": "logs.subscribe", "after_seq": 1042 } ``` #### Example: Snapshot Response ```json { "type": "logs.snapshot", "entries": [ { "seq": 1, "timestamp": "2025-03-30 14:22:01.123", "severity": "Info", "tag": "Server", "line": "2025-03-30 14:22:01.123 [Info] (Server) Starting Lemonade Server..." } ] } ``` #### Example: Live Entry ```json { "type": "logs.entry", "entry": { "seq": 1043, "timestamp": "2025-03-30 14:22:05.456", "severity": "Info", "tag": "Router", "line": "2025-03-30 14:22:05.456 [Info] (Router) Model loaded successfully" } } ``` #### Log Entry Fields | Field | Type | Description | |-------|------|-------------| | `seq` | integer | Monotonically increasing sequence number. Use for dedup and resume. | | `timestamp` | string | Formatted timestamp from the log system. | | `severity` | string | Log level: `Trace`, `Debug`, `Info`, `Warning`, `Error`, `Fatal`. | | `tag` | string | Log source tag (e.g., `Server`, `Router`, component name). | | `line` | string | The full formatted log line. | #### Integration Notes - **Reconnection**: Track the last `seq` received and pass it as `after_seq` on reconnect to avoid duplicate entries. - **Backlog**: The server retains up to 5000 recent log entries. The snapshot may be smaller if fewer entries exist. - **Platform availability**: WebSocket log streaming is available on all platforms (Windows, Linux, and macOS). ### `POST /api/v1/images/generations` ![Status](https://img.shields.io/badge/status-fully_available-green) Image Generation API. You provide a text prompt and receive a generated image. This API uses [stable-diffusion.cpp](https://github.com/leejet/stable-diffusion.cpp) as the backend. > **Note:** Image generation uses Stable Diffusion models. Available models include `SD-Turbo` (fast, ~4 steps), `SDXL-Turbo`, `SD-1.5`, and `SDXL-Base-1.0`. > > **Performance:** CPU inference takes ~4-5 minutes per image. GPU (Vulkan) is faster but may have compatibility issues with some hardware. #### Parameters | Parameter | Required | Description | Status | |-----------|----------|-------------|--------| | `prompt` | Yes | The text description of the image to generate. | ![Status](https://img.shields.io/badge/available-green) | | `model` | Yes | The Stable Diffusion model to use (e.g., `SD-Turbo`, `SDXL-Turbo`). | ![Status](https://img.shields.io/badge/available-green) | | `size` | No | The size of the generated image. Format: `WIDTHxHEIGHT` (e.g., `512x512`, `256x256`). Default: `512x512`. | ![Status](https://img.shields.io/badge/available-green) | | `n` | No | Number of images to generate. Currently only `1` is supported. | ![Status](https://img.shields.io/badge/partial-yellow) | | `response_format` | No | Format of the response. Only `b64_json` (base64-encoded image) is supported. | ![Status](https://img.shields.io/badge/partial-yellow) | | `steps` | No | Number of inference steps. SD-Turbo works well with 4 steps. Default varies by model. | ![Status](https://img.shields.io/badge/available-green) | | `cfg_scale` | No | Classifier-free guidance scale. SD-Turbo uses low values (~1.0). Default varies by model. | ![Status](https://img.shields.io/badge/available-green) | | `seed` | No | Random seed for reproducibility. If not specified, a random seed is used. | ![Status](https://img.shields.io/badge/available-green) | #### Example request === "Bash" ```bash curl -X POST http://localhost:13305/api/v1/images/generations \ -H "Content-Type: application/json" \ -d '{ "model": "SD-Turbo", "prompt": "A serene mountain landscape at sunset", "size": "512x512", "steps": 4, "response_format": "b64_json" }' ``` ### `POST /api/v1/images/edits` ![Status](https://img.shields.io/badge/status-fully_available-green) Image Editing API. You provide a source image and a text prompt describing the desired change, and receive an edited image. This API uses [stable-diffusion.cpp](https://github.com/leejet/stable-diffusion.cpp) as the backend. > **Note:** This endpoint accepts `multipart/form-data` requests (not JSON). Use editing-capable models such as `Flux-2-Klein-4B` or `SD-Turbo`. > > **Performance:** CPU inference takes several minutes per image. GPU (ROCm) is significantly faster. #### Parameters | Parameter | Required | Description | Status | |-----------|----------|-------------|--------| | `model` | Yes | The Stable Diffusion model to use (e.g., `Flux-2-Klein-4B`, `SD-Turbo`). | ![Status](https://img.shields.io/badge/available-green) | | `image` | Yes | The source image file to edit (PNG). Sent as a file in multipart/form-data. | ![Status](https://img.shields.io/badge/available-green) | | `prompt` | Yes | A text description of the desired edit. | ![Status](https://img.shields.io/badge/available-green) | | `mask` | No | An optional mask image (PNG). White areas indicate regions to edit; black areas are preserved. | ![Status](https://img.shields.io/badge/available-green) | | `size` | No | The size of the output image. Format: `WIDTHxHEIGHT` (e.g., `512x512`). Default: `512x512`. | ![Status](https://img.shields.io/badge/available-green) | | `n` | No | Number of images to generate. Allowed range: `1`โ€“`10`. Default: `1`. Values outside this range are rejected with `400 Bad Request`. | ![Status](https://img.shields.io/badge/partial-yellow) | | `response_format` | No | Format of the response. Only `b64_json` (base64-encoded image) is supported. | ![Status](https://img.shields.io/badge/partial-yellow) | | `steps` | No | Number of inference steps. Default varies by model. | ![Status](https://img.shields.io/badge/available-green) | | `cfg_scale` | No | Classifier-free guidance scale. Default varies by model. | ![Status](https://img.shields.io/badge/available-green) | | `seed` | No | Random seed for reproducibility. | ![Status](https://img.shields.io/badge/available-green) | | `user` | No | OpenAI API compatibility field. Accepted but not forwarded to the backend. | ![Status](https://img.shields.io/badge/not_available-red) | | `background` | No | OpenAI API compatibility field. Accepted but not forwarded to the backend. | ![Status](https://img.shields.io/badge/not_available-red) | | `quality` | No | OpenAI API compatibility field. Accepted but not forwarded to the backend. | ![Status](https://img.shields.io/badge/not_available-red) | | `input_fidelity` | No | OpenAI API compatibility field. Accepted but not forwarded to the backend. | ![Status](https://img.shields.io/badge/not_available-red) | | `output_compression` | No | OpenAI API compatibility field. Accepted; silently ignored by the backend. | ![Status](https://img.shields.io/badge/not_available-red) | #### Example request === "Bash" ```bash curl -X POST http://localhost:13305/api/v1/images/edits \ -F "model=Flux-2-Klein-4B" \ -F "prompt=Add a red barn and mountains in the background, photorealistic" \ -F "size=512x512" \ -F "n=1" \ -F "response_format=b64_json" \ -F "image=@/path/to/source_image.png" ``` === "Python (OpenAI client)" ```python from openai import OpenAI client = OpenAI(base_url="http://localhost:13305/api/v1", api_key="not-needed") with open("source_image.png", "rb") as image_file: response = client.images.edit( model="Flux-2-Klein-4B", image=image_file, prompt="Add a red barn and mountains in the background, photorealistic", size="512x512", ) import base64 image_data = base64.b64decode(response.data[0].b64_json) open("edited_image.png", "wb").write(image_data) ``` --- ### `POST /api/v1/images/variations` ![Status](https://img.shields.io/badge/status-fully_available-green) Image Variations API. You provide a source image and receive a variation of it. This API uses [stable-diffusion.cpp](https://github.com/leejet/stable-diffusion.cpp) as the backend. > **Note:** This endpoint accepts `multipart/form-data` requests (not JSON). Unlike `/images/edits`, a `prompt` parameter is not supported and will be ignored โ€” the model generates a variation based solely on the input image. > > **Performance:** CPU inference takes several minutes per image. GPU (ROCm) is significantly faster. #### Parameters | Parameter | Required | Description | Status | |-----------|----------|-------------|--------| | `model` | Yes | The Stable Diffusion model to use (e.g., `Flux-2-Klein-4B`, `SD-Turbo`). | ![Status](https://img.shields.io/badge/available-green) | | `image` | Yes | The source image file (PNG). Sent as a file in multipart/form-data. | ![Status](https://img.shields.io/badge/available-green) | | `size` | No | The size of the output image. Format: `WIDTHxHEIGHT` (e.g., `512x512`). Default: `512x512`. | ![Status](https://img.shields.io/badge/available-green) | | `n` | No | Number of variations to generate. Integer between 1 and 10 inclusive. Default: `1`. Values outside this range result in a 400 Bad Request error. | ![Status](https://img.shields.io/badge/partial-yellow) | | `response_format` | No | Format of the response. Only `b64_json` (base64-encoded image) is supported. | ![Status](https://img.shields.io/badge/partial-yellow) | | `user` | No | OpenAI API compatibility field. Accepted but not forwarded to the backend. | ![Status](https://img.shields.io/badge/not_available-red) | #### Example request === "Bash" ```bash curl -X POST http://localhost:13305/api/v1/images/variations \ -F "model=Flux-2-Klein-4B" \ -F "size=512x512" \ -F "n=1" \ -F "response_format=b64_json" \ -F "image=@/path/to/source_image.png" ``` === "Python (OpenAI client)" ```python from openai import OpenAI client = OpenAI(base_url="http://localhost:13305/api/v1", api_key="not-needed") with open("source_image.png", "rb") as image_file: response = client.images.create_variation( model="Flux-2-Klein-4B", image=image_file, size="512x512", n=1, ) import base64 image_data = base64.b64decode(response.data[0].b64_json) open("variation.png", "wb").write(image_data) ``` --- ### `POST /api/v1/images/upscale` ![Status](https://img.shields.io/badge/status-fully_available-green) Image Upscaling API. You provide a base64-encoded image and a Real-ESRGAN model name, and receive a 4x upscaled image. This API uses the `sd-cli` binary from [stable-diffusion.cpp](https://github.com/leejet/stable-diffusion.cpp) to perform super-resolution. > **Note:** Available upscale models are `RealESRGAN-x4plus` (general-purpose, 64 MB) and `RealESRGAN-x4plus-anime` (optimized for anime-style art, 17 MB). Both produce a 4x resolution increase (e.g., 256x256 โ†’ 1024x1024). > > **Note:** Unlike `/images/edits` and `/images/variations`, this endpoint accepts a JSON body (not multipart/form-data). The image must be provided as a base64-encoded string. #### Parameters | Parameter | Required | Description | Status | |-----------|----------|-------------|--------| | `image` | Yes | Base64-encoded PNG image to upscale. | ![Status](https://img.shields.io/badge/available-green) | | `model` | Yes | The ESRGAN model to use (e.g., `RealESRGAN-x4plus`, `RealESRGAN-x4plus-anime`). | ![Status](https://img.shields.io/badge/available-green) | #### Example request A typical workflow is to generate an image first, then upscale it: === "Bash" ```bash # Step 1: Generate an image and save the base64 response RESPONSE=$(curl -s -X POST http://localhost:13305/api/v1/images/generations \ -H "Content-Type: application/json" \ -d '{ "model": "SD-Turbo", "prompt": "A serene mountain landscape at sunset", "size": "512x512", "steps": 4, "response_format": "b64_json" }') # Step 2: Build the upscale JSON payload and pipe it to curl via stdin # (base64 images are too large for command-line interpolation) echo "$RESPONSE" | python3 -c " import sys, json b64 = json.load(sys.stdin)['data'][0]['b64_json'] print(json.dumps({'image': b64, 'model': 'RealESRGAN-x4plus'})) " | curl -X POST http://localhost:13305/api/v1/images/upscale \ -H "Content-Type: application/json" \ -d @- ``` === "PowerShell" ```powershell # Step 1: Generate an image $genResponse = Invoke-WebRequest ` -Uri "http://localhost:13305/api/v1/images/generations" ` -Method POST ` -Headers @{ "Content-Type" = "application/json" } ` -Body '{ "model": "SD-Turbo", "prompt": "A serene mountain landscape at sunset", "size": "512x512", "steps": 4, "response_format": "b64_json" }' # Step 2: Extract the base64 image $imageB64 = ($genResponse.Content | ConvertFrom-Json).data[0].b64_json # Step 3: Upscale the image with Real-ESRGAN $body = @{ image = $imageB64; model = "RealESRGAN-x4plus" } | ConvertTo-Json Invoke-WebRequest ` -Uri "http://localhost:13305/api/v1/images/upscale" ` -Method POST ` -Headers @{ "Content-Type" = "application/json" } ` -Body $body ``` === "Python (requests)" ```python import requests import base64 BASE_URL = "http://localhost:13305/api/v1" # Step 1: Generate an image gen_response = requests.post(f"{BASE_URL}/images/generations", json={ "model": "SD-Turbo", "prompt": "A serene mountain landscape at sunset", "size": "512x512", "steps": 4, "response_format": "b64_json", }) image_b64 = gen_response.json()["data"][0]["b64_json"] # Step 2: Upscale the image with Real-ESRGAN (512x512 -> 2048x2048) upscale_response = requests.post(f"{BASE_URL}/images/upscale", json={ "image": image_b64, "model": "RealESRGAN-x4plus", }) # Step 3: Save the upscaled image to a file upscaled_b64 = upscale_response.json()["data"][0]["b64_json"] with open("upscaled.png", "wb") as f: f.write(base64.b64decode(upscaled_b64)) ``` #### Response format ```json { "created": 1742927481, "data": [ { "b64_json": "" } ] } ``` **Field Descriptions:** - `created` - Unix timestamp of when the upscaled image was generated - `data` - Array containing the upscaled image - `b64_json` - Base64-encoded PNG of the upscaled image #### Error responses | Status Code | Condition | Example | |-------------|-----------|---------| | 400 | Missing `image` field | `{"error": {"message": "Missing 'image' field (base64 encoded)", "type": "invalid_request_error"}}` | | 400 | Missing `model` field | `{"error": {"message": "Missing 'model' field", "type": "invalid_request_error"}}` | | 404 | Unknown model name | `{"error": {"message": "Upscale model not found: bad-model", "type": "invalid_request_error"}}` | | 500 | Upscale failed | `{"error": {"message": "ESRGAN upscale failed", "type": "server_error"}}` | --- ### `POST /api/v1/audio/speech` ![Status](https://img.shields.io/badge/status-fully_available-green) Speech Generation API. You provide a text input and receive an audio file. This API uses [Kokoros](https://github.com/lucasjinreal/Kokoros) as the backend. > **Note:** The model to use is called `kokoro-v1`. No other model is supported at the moment. > > **Limitations:** Only `mp3`, `wav`, `opus`, and `pcm` are supported. Streaming is supported in `audio` (`pcm`) mode. #### Parameters | Parameter | Required | Description | Status | |-----------|----------|-------------|--------| | `input` | Yes | The text to speak. | ![Status](https://img.shields.io/badge/available-green) | | `model` | Yes | The model to use (e.g., `kokoro-v1`). | ![Status](https://img.shields.io/badge/available-green) | | `speed` | No | Speaking speed. Default: `1.0`. | ![Status](https://img.shields.io/badge/available-green) | | `voice` | No | The voice to use. All OpenAI-defined voices can be used (`alloy`, `ash`, ...), as well as those defined by the kokoro model (`af_sky`, `am_echo`, ...). Default: `shimmer` | ![Status](https://img.shields.io/badge/partial-yellow) | | `response_format` | No | Format of the response. `mp3`, `wav`, `opus`, and `pcm` are supported. Default: `mp3`| ![Status](https://img.shields.io/badge/partial-yellow) | | `stream_format` | No | If set, the response will be streamed. Only `audio` is supported, which will output `pcm` audio. Default: not set| ![Status](https://img.shields.io/badge/partial-yellow) | #### Example request === "Bash" ```bash curl -X POST http://localhost:13305/api/v1/audio/speech \ -H "Content-Type: application/json" \ -d '{ "model": "kokoro-v1", "input": "Lemonade can speak!", "speed": 1.0, "steps": 4, "response_format": "mp3" }' ``` #### Response format The generated audio file is returned as-is. ### `GET /api/v1/models` ![Status](https://img.shields.io/badge/status-fully_available-green) Returns a list of models available on the server in an OpenAI-compatible format. Each model object includes extended fields like `checkpoint`, `recipe`, `size`, `downloaded`, and `labels`. By default, only models available locally (downloaded) are shown, matching OpenAI API behavior. #### Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `show_all` | No | If set to `true`, returns all models from the catalog including those not yet downloaded. Defaults to `false`. | #### Example request ```bash # Show only downloaded models (OpenAI-compatible) curl http://localhost:13305/api/v1/models # Show all models including not-yet-downloaded (extended usage) curl http://localhost:13305/api/v1/models?show_all=true ``` #### Response format ```json { "object": "list", "data": [ { "id": "Qwen3-0.6B-GGUF", "created": 1744173590, "object": "model", "owned_by": "lemonade", "checkpoint": "unsloth/Qwen3-0.6B-GGUF:Q4_0", "recipe": "llamacpp", "size": 0.38, "downloaded": true, "suggested": true, "labels": ["reasoning"] }, { "id": "Gemma-3-4b-it-GGUF", "created": 1744173590, "object": "model", "owned_by": "lemonade", "checkpoint": "ggml-org/gemma-3-4b-it-GGUF:Q4_K_M", "recipe": "llamacpp", "size": 3.61, "downloaded": true, "suggested": true, "labels": ["hot", "vision"] }, { "id": "SD-Turbo", "created": 1744173590, "object": "model", "owned_by": "lemonade", "checkpoint": "stabilityai/sd-turbo:sd_turbo.safetensors", "recipe": "sd-cpp", "size": 5.2, "downloaded": true, "suggested": true, "labels": ["image"], "image_defaults": { "steps": 4, "cfg_scale": 1.0, "width": 512, "height": 512 } } ] } ``` **Field Descriptions:** - `object` - Type of response object, always `"list"` - `data` - Array of model objects with the following fields: - `id` - Model identifier (used for loading and inference requests) - `created` - Unix timestamp of when the model entry was created - `object` - Type of object, always `"model"` - `owned_by` - Owner of the model, always `"lemonade"` - `checkpoint` - Full checkpoint identifier on Hugging Face - `recipe` - Backend/device recipe used to load the model (e.g., `"ryzenai-llm"`, `"llamacpp"`, `"flm"`) - `size` - Model size in GB (omitted for models without size information) - `downloaded` - Boolean indicating if the model is downloaded and available locally - `suggested` - Boolean indicating if the model is recommended for general use - `labels` - Array of tags describing the model (e.g., `"hot"`, `"reasoning"`, `"vision"`, `"embeddings"`, `"reranking"`, `"coding"`, `"tool-calling"`, `"image"`) - `image_defaults` - (Image models only) Default generation parameters for the model: - `steps` - Number of inference steps (e.g., 4 for turbo models, 20 for standard models) - `cfg_scale` - Classifier-free guidance scale (e.g., 1.0 for turbo models, 7.5 for standard models) - `width` - Default image width in pixels - `height` - Default image height in pixels ### `GET /api/v1/models/{model_id}` ![Status](https://img.shields.io/badge/status-fully_available-green) Retrieve a specific model by its ID. Returns the same model object format as the list endpoint above. #### Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `model_id` | Yes | The ID of the model to retrieve. Must match one of the model IDs from the [models list](https://lemonade-server.ai/models.html). | #### Example request ```bash curl http://localhost:13305/api/v1/models/Qwen3-0.6B-GGUF ``` #### Response format Returns a single model object with the same fields as described in the [models list endpoint](#get-apiv1models) above. ```json { "id": "Qwen3-0.6B-GGUF", "created": 1744173590, "object": "model", "owned_by": "lemonade", "checkpoint": "unsloth/Qwen3-0.6B-GGUF:Q4_0", "recipe": "llamacpp", "size": 0.38, "downloaded": true, "suggested": true, "labels": ["reasoning"], "recipe_options" { "ctx_size": 8192, "llamacpp_args": "--no-mmap", "llamacpp_backend": "rocm" } } ``` #### Error responses If the model is not found, the endpoint returns a 404 error: ```json { "error": { "message": "Model Qwen3-0.6B-GGUF has not been found", "type": "not_found" } } ``` ## Additional Endpoints ### `POST /api/v1/pull` ![Status](https://img.shields.io/badge/status-fully_available-green) Register and install models for use with Lemonade Server. #### Parameters The Lemonade Server built-in model registry has a collection of model names that can be pulled and loaded. The `pull` endpoint can install any registered model, and it can also register-then-install any model available on Hugging Face. **Common Parameters** | Parameter | Required | Description | |-----------|----------|-------------| | `stream` | No | If `true`, returns Server-Sent Events (SSE) with download progress. Defaults to `false`. | **Install a Model that is Already Registered** | Parameter | Required | Description | |-----------|----------|-------------| | `model_name` | Yes | [Lemonade Server model name](https://lemonade-server.ai/models.html) to install. | Example request: ```bash curl -X POST http://localhost:13305/api/v1/pull \ -H "Content-Type: application/json" \ -d '{ "model_name": "Qwen2.5-0.5B-Instruct-CPU" }' ``` Response format: ```json { "status":"success", "message":"Installed model: Qwen2.5-0.5B-Instruct-CPU" } ``` In case of an error, the status will be `error` and the message will contain the error message. **Register and Install a Model** Registration will place an entry for that model in the `user_models.json` file, which is located in the user's Lemonade cache (default: `~/.cache/lemonade`). Then, the model will be installed. Once the model is registered and installed, it will show up in the `models` endpoint alongside the built-in models and can be loaded. The `recipe` field defines which software framework and device will be used to load and run the model. For more information on OGA and Hugging Face recipes, see the [Lemonade API README](../lemonade_api.md). For information on GGUF recipes, see [llamacpp](#gguf-support). > Note: the `model_name` for registering a new model must use the `user` namespace, to prevent collisions with built-in models. For example, `user.Phi-4-Mini-GGUF`. | Parameter | Required | Description | |-----------|----------|-------------| | `model_name` | Yes | Namespaced [Lemonade Server model name](https://lemonade-server.ai/models.html) to register and install. | | `checkpoint` | Yes | HuggingFace checkpoint to install. | | `recipe` | Yes | Lemonade API recipe to load the model with. | | `reasoning` | No | Whether the model is a reasoning model, like DeepSeek (default: false). Adds 'reasoning' label. | | `vision` | No | Whether the model has vision capabilities for processing images (default: false). Adds 'vision' label. | | `embedding` | No | Whether the model is an embedding model (default: false). Adds 'embeddings' label. | | `reranking` | No | Whether the model is a reranking model (default: false). Adds 'reranking' label. | | `mmproj` | No | Multimodal Projector (mmproj) file to use for vision models. | Example request: ```bash curl -X POST http://localhost:13305/api/v1/pull \ -H "Content-Type: application/json" \ -d '{ "model_name": "user.Phi-4-Mini-GGUF", "checkpoint": "unsloth/Phi-4-mini-instruct-GGUF:Q4_K_M", "recipe": "llamacpp" }' ``` Response format: ```json { "status":"success", "message":"Installed model: user.Phi-4-Mini-GGUF" } ``` In case of an error, the status will be `error` and the message will contain the error message. #### Streaming Response (stream=true) When `stream=true`, the endpoint returns Server-Sent Events with real-time download progress: ``` event: progress data: {"file":"model.gguf","file_index":1,"total_files":2,"bytes_downloaded":1073741824,"bytes_total":2684354560,"percent":40} event: progress data: {"file":"config.json","file_index":2,"total_files":2,"bytes_downloaded":1024,"bytes_total":1024,"percent":100} event: complete data: {"file_index":2,"total_files":2,"percent":100} ``` **Event Types:** | Event | Description | |-------|-------------| | `progress` | Sent during download with current file and byte progress | | `complete` | Sent when all files are downloaded successfully | | `error` | Sent if download fails, with `error` field containing the message | ### `GET /api/v1/pull/variants` ![Status](https://img.shields.io/badge/status-fully_available-green) Inspect a Hugging Face GGUF repository and enumerate the variants (quantizations and sharded folder groups) available for installation. Used by the `lemonade pull ` CLI flow and by the desktop app's model search to auto-populate the install form. The endpoint reads only public Hugging Face metadata; if the `HF_TOKEN` environment variable is set on the server, it is forwarded as a bearer token to access gated repositories. #### Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `checkpoint` | Yes | Hugging Face repo id, e.g. `unsloth/Qwen3-8B-GGUF`. Passed as a query string. | Example request: ```bash curl 'http://localhost:13305/api/v1/pull/variants?checkpoint=unsloth/Qwen3-8B-GGUF' ``` #### Response ```json { "checkpoint": "unsloth/Qwen3-8B-GGUF", "recipe": "llamacpp", "suggested_name": "Qwen3-8B-GGUF", "suggested_labels": ["vision"], "mmproj_files": ["mmproj-model-f16.gguf"], "variants": [ { "name": "Q4_K_M", "primary_file": "Qwen3-8B-Q4_K_M.gguf", "files": ["Qwen3-8B-Q4_K_M.gguf"], "sharded": false, "size_bytes": 4920000000 }, { "name": "Q8_0", "primary_file": "Q8_0/Qwen3-8B-Q8_0-00001-of-00002.gguf", "files": ["Q8_0/Qwen3-8B-Q8_0-00001-of-00002.gguf", "Q8_0/Qwen3-8B-Q8_0-00002-of-00002.gguf"], "sharded": true, "size_bytes": 8500000000 } ] } ``` | Field | Description | |-------|-------------| | `checkpoint` | Echoed input. | | `recipe` | Suggested recipe (always `llamacpp` today; future expansion may return other values). | | `suggested_name` | Repo id stripped of the `owner/` prefix; suitable for use as the `user.` model name. | | `suggested_labels` | Inferred labels โ€” `vision` if any `mmproj-*.gguf` files exist, plus `embeddings`/`reranking` if those substrings appear in the repo id. | | `mmproj_files` | Bare filenames of `mmproj-*.gguf` files in the repo; the first one should be passed as `mmproj` to `/api/v1/pull` for vision models. | | `variants[]` | Top quantizations for the repo, capped at 5. Each entry has `name` (e.g. `Q4_K_M`, `UD-Q4_K_XL`), `primary_file`, `files`, `sharded`, and `size_bytes` (from the HF `?blobs=true` listing). Ranked by frequency of use in `server_models.json` (`Q4_K_M`, `UD-Q4_K_XL`, `Q8_0`, `Q4_0` first, everything else sorted lexicographically). The CLI `lemonade pull` menu adds a free-text "Other" option for quants outside the top 5. | #### Error responses | Status | Cause | |--------|-------| | 400 | `checkpoint` query parameter missing or malformed (must contain `/`). | | 404 | Hugging Face returned 404 for the checkpoint. | | 500 | Other transport or parsing failures; the response body contains an `error` message. | ### `POST /api/v1/delete` ![Status](https://img.shields.io/badge/status-fully_available-green) Delete a model by removing it from local storage. If the model is currently loaded, it will be unloaded first. #### Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `model_name` | Yes | [Lemonade Server model name](https://lemonade-server.ai/models.html) to delete. | Example request: ```bash curl -X POST http://localhost:13305/api/v1/delete \ -H "Content-Type: application/json" \ -d '{ "model_name": "Qwen2.5-0.5B-Instruct-CPU" }' ``` Response format: ```json { "status":"success", "message":"Deleted model: Qwen2.5-0.5B-Instruct-CPU" } ``` In case of an error, the status will be `error` and the message will contain the error message. ### `POST /api/v1/load` ![Status](https://img.shields.io/badge/status-fully_available-green) Explicitly load a registered model into memory. This is useful to ensure that the model is loaded before you make a request. Installs the model if necessary. #### Parameters | Parameter | Required | Applies to | Description | |-----------|----------|------------|-------------| | `model_name` | Yes | All | [Lemonade Server model name](https://lemonade-server.ai/models.html) to load. | | `save_options` | No | All | Boolean. If true, saves recipe options to `recipe_options.json`. Any previously stored value for `model_name` is replaced. | | `ctx_size` | No | llamacpp, flm, ryzenai-llm | Context size for the model. Overrides the default value. | | `llamacpp_backend` | No | llamacpp | LlamaCpp backend to use (`vulkan`, `rocm`, `metal` or `cpu`). | | `llamacpp_args` | No | llamacpp | Custom arguments to pass to llama-server. The following are NOT allowed: `-m`, `--port`, `--ctx-size`, `-ngl`, `--jinja`, `--mmproj`, `--embeddings`, `--reranking`. | | `whispercpp_backend` | No | whispercpp | WhisperCpp backend: `npu` or `cpu` on Windows; `cpu` or `vulkan` on Linux. Default is `npu` if supported. | | `whispercpp_args` | No | whispercpp | Custom arguments to pass to whisper-server. The following are NOT allowed: `-m`, `--model`, `--port`. Example: `--convert`. | | `steps` | No | sd-cpp | Number of inference steps for image generation. Default: 20. | | `cfg_scale` | No | sd-cpp | Classifier-free guidance scale for image generation. Default: 7.0. | | `width` | No | sd-cpp | Image width in pixels. Default: 512. | | `height` | No | sd-cpp | Image height in pixels. Default: 512. | **Setting Priority:** When loading a model, settings are applied in this priority order: 1. Values explicitly passed in the `load` request (highest priority) 2. Per-model values configurable in `recipe_options.json` (see below for details) 3. Values from environment variables or server startup arguments (see [Server Configuration](./configuration.md)) 4. Default hardcoded values in `lemond` (lowest priority) #### Per-model options You can configure recipe-specific options on a per-model basis. Lemonade manages a file called `recipe_options.json` in the user's Lemonade cache (default: `~/.cache/lemonade`). The available options depend on the model's recipe: ```json { "user.Qwen2.5-Coder-1.5B-Instruct": { "ctx_size": 16384, "llamacpp_backend": "vulkan", "llamacpp_args": "-np 2 -kvu" }, "Qwen3-Coder-30B-A3B-Instruct-GGUF" : { "llamacpp_backend": "rocm" }, "whisper-large-v3-turbo-q8_0.bin": { "whispercpp_backend": "npu", "whispercpp_args": "--convert" } } ``` Note that model names include any applicable prefix, such as `user.` and `extra.`. #### Example requests Basic load: ```bash curl -X POST http://localhost:13305/api/v1/load \ -H "Content-Type: application/json" \ -d '{ "model_name": "Qwen2.5-0.5B-Instruct-CPU" }' ``` Load with custom settings: ```bash curl -X POST http://localhost:13305/api/v1/load \ -H "Content-Type: application/json" \ -d '{ "model_name": "Qwen3-0.6B-GGUF", "ctx_size": 8192, "llamacpp_backend": "rocm", "llamacpp_args": "--flash-attn on --no-mmap" }' ``` Load and save settings: ```bash curl -X POST http://localhost:13305/api/v1/load \ -H "Content-Type: application/json" \ -d '{ "model_name": "Qwen3-0.6B-GGUF", "ctx_size": 8192, "llamacpp_backend": "vulkan", "llamacpp_args": "--no-context-shift --no-mmap", "save_options": true }' ``` Load a Whisper model with NPU backend and conversion enabled: ```bash curl -X POST http://localhost:13305/api/v1/load \ -H "Content-Type: application/json" \ -d '{ "model_name": "whisper-large-v3-turbo-q8_0.bin", "whispercpp_backend": "npu", "whispercpp_args": "--convert" }' ``` Load an image generation model with custom settings: ```bash curl -X POST http://localhost:13305/api/v1/load \ -H "Content-Type: application/json" \ -d '{ "model_name": "sd-turbo", "steps": 4, "cfg_scale": 1.0, "width": 512, "height": 512 }' ``` #### Response format ```json { "status":"success", "message":"Loaded model: Qwen2.5-0.5B-Instruct-CPU" } ``` In case of an error, the status will be `error` and the message will contain the error message. ### `POST /api/v1/unload` ![Status](https://img.shields.io/badge/status-fully_available-green) Explicitly unload a model from memory. This is useful to free up memory while still leaving the server process running (which takes minimal resources but a few seconds to start). #### Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `model_name` | No | Name of the specific model to unload. If not provided, all loaded models will be unloaded. | #### Example requests Unload a specific model: ```bash curl -X POST http://localhost:13305/api/v1/unload \ -H "Content-Type: application/json" \ -d '{"model_name": "Qwen3-0.6B-GGUF"}' ``` Unload all models: ```bash curl -X POST http://localhost:13305/api/v1/unload ``` #### Response format Success response: ```json { "status": "success", "message": "Model unloaded successfully" } ``` Error response (model not found): ```json { "status": "error", "message": "Model not found: Qwen3-0.6B-GGUF" } ``` In case of an error, the status will be `error` and the message will contain the error message. ### `GET /api/v1/health` ![Status](https://img.shields.io/badge/status-fully_available-green) Check the health of the server. This endpoint returns information about loaded models. #### Parameters This endpoint does not take any parameters. #### Example request ```bash curl http://localhost:13305/api/v1/health ``` #### Response format ```json { "status": "ok", "version":"9.3.3", "websocket_port":9000, "model_loaded": "Llama-3.2-1B-Instruct-Hybrid", "all_models_loaded": [ { "model_name": "Llama-3.2-1B-Instruct-Hybrid", "checkpoint": "amd/Llama-3.2-1B-Instruct-awq-g128-int4-asym-fp16-onnx-hybrid", "last_use": 1732123456.789, "type": "llm", "device": "gpu npu", "recipe": "ryzenai-llm", "recipe_options": { "ctx_size": 4096 }, "backend_url": "http://127.0.0.1:8001/v1" }, { "model_name": "nomic-embed-text-v1-GGUF", "checkpoint": "nomic-ai/nomic-embed-text-v1-GGUF:Q4_K_S", "last_use": 1732123450.123, "type": "embedding", "device": "gpu", "recipe": "llamacpp", "recipe_options": { "ctx_size": 8192, "llamacpp_args": "--no-mmap", "llamacpp_backend": "rocm" }, "backend_url": "http://127.0.0.1:8002/v1" } ], "max_models": { "audio":1, "embedding":1, "image":1, "llm":1, "reranking":1, "tts":1 } } ``` **Field Descriptions:** - `status` - Server health status, always `"ok"` - `version` - Version number of Lemonade Server - `model_loaded` - Model name of the most recently accessed model - `all_models_loaded` - Array of all currently loaded models with details: - `model_name` - Name of the loaded model - `checkpoint` - Full checkpoint identifier - `last_use` - Unix timestamp of last access (load or inference) - `type` - Model type: `"llm"`, `"embedding"`, or `"reranking"` - `device` - Space-separated device list: `"cpu"`, `"gpu"`, `"npu"`, or combinations like `"gpu npu"` - `backend_url` - URL of the backend server process handling this model (useful for debugging) - `recipe`: - Backend/device recipe used to load the model (e.g., `"ryzenai-llm"`, `"llamacpp"`, `"flm"`) - `recipe_options`: - Options used to load the model (e.g., `"ctx_size"`, `"llamacpp_backend"`, `"llamacpp_args"`, `"whispercpp_args"`) - `max_models` - Maximum number of models that can be loaded simultaneously per type (set via `max_loaded_models` in [Server Configuration](./configuration.md)): - `llm` - Maximum LLM/chat models - `embedding` - Maximum embedding models - `reranking` - Maximum reranking models - `audio` - Maximum speech-to-text models - `image` - Maximum image models - `tts` - Maximum text-to-speech models - `websocket_port` - *(optional)* Port of the WebSocket server for the [Realtime Audio Transcription API](#realtime-audio-transcription-api-websocket) and [Log Streaming API](#log-streaming-api-websocket). Only present when the WebSocket server is running. The port is OS-assigned or set via `--websocket-port`. ### `GET /api/v1/stats` ![Status](https://img.shields.io/badge/status-fully_available-green) Performance statistics from the last request. #### Parameters This endpoint does not take any parameters. #### Example request ```bash curl http://localhost:13305/api/v1/stats ``` #### Response format ```json { "time_to_first_token": 2.14, "tokens_per_second": 33.33, "input_tokens": 128, "output_tokens": 5, "decode_token_times": [0.01, 0.02, 0.03, 0.04, 0.05], "prompt_tokens": 9 } ``` **Field Descriptions:** - `time_to_first_token` - Time in seconds until the first token was generated - `tokens_per_second` - Generation speed in tokens per second - `input_tokens` - Number of tokens processed - `output_tokens` - Number of tokens generated - `decode_token_times` - Array of time taken for each generated token - `prompt_tokens` - Total prompt tokens including cached tokens ### `GET /api/v1/system-info` ![Status](https://img.shields.io/badge/status-fully_available-green) System information endpoint that provides complete hardware details and device enumeration. #### Example request ```bash curl "http://localhost:13305/api/v1/system-info" ``` #### Response format ```json { "OS Version": "Windows-10-10.0.26100-SP0", "Processor": "AMD Ryzen AI 9 HX 375 w/ Radeon 890M", "Physical Memory": "32.0 GB", "OEM System": "ASUS Zenbook S 16", "BIOS Version": "1.0.0", "CPU Max Clock": "5100 MHz", "Windows Power Setting": "Balanced", "devices": { "cpu": { "name": "AMD Ryzen AI 9 HX 375 w/ Radeon 890M", "cores": 12, "threads": 24, "available": true, "family": "x86_64" }, "amd_igpu": { "name": "AMD Radeon(TM) 890M Graphics", "vram_gb": 0.5, "available": true, "family": "gfx1150" }, "amd_dgpu": [], "amd_npu": { "name": "AMD Ryzen AI 9 HX 375 w/ Radeon 890M", "power_mode": "Default", "available": true, "family": "XDNA2" } }, "recipes": { "llamacpp": { "default_backend": "vulkan", "backends": { "vulkan": { "devices": ["cpu", "amd_igpu"], "state": "installed", "message": "", "action": "", "version": "b7869" }, "rocm": { "devices": ["amd_igpu"], "state": "installable", "message": "Backend is supported but not installed.", "action": "lemonade backends install llamacpp:rocm" }, "metal": { "devices": [], "state": "unsupported", "message": "Requires macOS", "action": "" }, "cpu": { "devices": ["cpu"], "state": "update_required", "message": "Backend update is required before use.", "action": "lemonade backends install llamacpp:cpu" } } }, "whispercpp": { "default_backend": "default", "backends": { "default": { "devices": ["cpu"], "state": "installable", "message": "Backend is supported but not installed.", "action": "lemonade backends install whispercpp:default" } } }, "sd-cpp": { "default_backend": "default", "backends": { "default": { "devices": ["cpu"], "state": "installable", "message": "Backend is supported but not installed.", "action": "lemonade backends install sd-cpp:default" } } }, "flm": { "default_backend": "default", "backends": { "default": { "devices": ["amd_npu"], "state": "installed", "message": "", "action": "", "version": "1.2.0" } } }, "ryzenai-llm": { "default_backend": "default", "backends": { "default": { "devices": ["amd_npu"], "state": "installed", "message": "", "action": "" } } } } } ``` **Field Descriptions:** - **System fields:** - `OS Version` - Operating system name and version - `Processor` - CPU model name - `Physical Memory` - Total RAM - `OEM System` - System/laptop model name (Windows only) - `BIOS Version` - BIOS information (Windows only) - `CPU Max Clock` - Maximum CPU clock speed (Windows only) - `Windows Power Setting` - Current power plan (Windows only) - `devices` - Hardware devices detected on the system (no software/support information) - `cpu` - CPU information (name, cores, threads) - `amd_igpu` - AMD integrated GPU (if present) - `amd_dgpu` - Array of AMD discrete GPUs (if present) - `nvidia_dgpu` - Array of NVIDIA discrete GPUs (if present) - `amd_npu` - AMD NPU device (if present) - `recipes` - Software recipes and their backend support status - Each recipe (e.g., `llamacpp`, `whispercpp`, `flm`) contains: - `default_backend` - Preferred backend selected by server policy for this system (present when at least one backend is not `unsupported`) - `backends` - Available backends for this recipe - Each backend contains: - `devices` - List of devices **on this system** that support this backend (empty if not supported) - `state` - Backend lifecycle state: `unsupported`, `installable`, `update_required`, or `installed` - `message` - Human-readable status text for GUI and CLI users. Required for `unsupported`, `installable`, and `update_required`; empty for `installed`. - `action` - Actionable user instruction string. For install/update cases this is typically an exact CLI command; for other states it may be empty or another actionable value (for example, a URL). - `version` - Installed or configured backend version (when available) ### `POST /api/v1/install` ![Status](https://img.shields.io/badge/status-fully_available-green) Install or update a backend for a specific recipe/backend pair. If the backend is already installed but outdated, this endpoint updates it to the configured version. #### Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `recipe` | Yes | Recipe name (for example, `llamacpp`, `flm`, `whispercpp`, `sd-cpp`, `ryzenai-llm`) | | `backend` | Yes | Backend name within the recipe (for example, `vulkan`, `rocm`, `cpu`, `default`) | | `stream` | No | If `true`, returns Server-Sent Events with progress. Defaults to `false`. | | `force` | No | If `true`, bypasses hardware filtering for `unsupported` backends and attempts installation anyway. Defaults to `false`. | #### Example request ```bash curl -X POST http://localhost:13305/api/v1/install \ -H "Content-Type: application/json" \ -d '{ "recipe": "llamacpp", "backend": "vulkan", "stream": false }' ``` #### Response format ```json { "status":"success", "recipe":"llamacpp", "backend":"vulkan" } ``` In case of an error, returns an `error` field with details. ### `POST /api/v1/uninstall` ![Status](https://img.shields.io/badge/status-fully_available-green) Uninstall a backend for a specific recipe/backend pair. If loaded models are using that backend, they are unloaded first. #### Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `recipe` | Yes | Recipe name | | `backend` | Yes | Backend name | #### Example request ```bash curl -X POST http://localhost:13305/api/v1/uninstall \ -H "Content-Type: application/json" \ -d '{ "recipe": "llamacpp", "backend": "vulkan" }' ``` #### Response format ```json { "status":"success", "recipe":"llamacpp", "backend":"vulkan" } ``` In case of an error, returns an `error` field with details. # Debugging To control logging verbosity, use `lemonade config set log_level=debug` (see [Server Configuration](./configuration.md)). Available levels: - **critical**: Only critical errors that prevent server operation. - **error**: Error conditions that might allow continued operation. - **warning**: Warning conditions that should be addressed. - **info**: (Default) General informational messages about server operation. - **debug**: Detailed diagnostic information for troubleshooting, including metrics such as input/output token counts, Time To First Token (TTFT), and Tokens Per Second (TPS). - **trace**: Very detailed tracing information, including everything from debug level plus all input prompts. # GGUF Support The `llama-server` backend works with Lemonade's suggested `*-GGUF` models, as well as any .gguf model from Hugging Face. Windows and Ubuntu Linux are supported. Details: - Lemonade Server wraps `llama-server` with support for the `lemonade` CLI, client web app, and endpoints (e.g., `models`, `pull`, `load`, etc.). - The `chat/completions`, `completions`, `embeddings`, and `reranking` endpoints are supported. - The `embeddings` endpoint requires embedding-specific models (e.g., nomic-embed-text models). - The `reranking` endpoint requires reranker-specific models (e.g., bge-reranker models). - `responses` is not supported at this time. - A single Lemonade Server process can seamlessly switch between GGUF, ONNX, and FastFlowLM models. - Lemonade Server will attempt to load models onto GPU with Vulkan first, and if that doesn't work it will fall back to CPU. - From the end-user's perspective, OGA vs. GGUF should be completely transparent: they wont be aware of whether the built-in server or `llama-server` is serving their model. ## Installing GGUF Models To install an arbitrary GGUF from Hugging Face, open the Lemonade web app by navigating to http://localhost:13305 in your web browser, click the Model Management tab, and use the Add a Model form. ## Platform Support Matrix | Platform | GPU Acceleration | CPU Architecture | |----------|------------------|------------------| | Windows | โœ… Vulkan, ROCm | โœ… x64 | | Ubuntu | โœ… Vulkan, ROCm | โœ… x64 | | Other Linux | โš ๏ธ* Vulkan | โš ๏ธ* x64 | *Other Linux distributions may work but are not officially supported. # FastFlowLM Support Similar to the [llama-server support](#gguf-support), Lemonade can also route OpenAI API requests to a FastFlowLM `flm serve` backend. The `flm serve` backend works with Lemonade's suggested `*-FLM` models, as well as any model mentioned in `flm list`. Windows is the only supported operating system. Details: - Lemonade Server wraps `flm serve` with support for the `lemonade` CLI, client web app, and all Lemonade custom endpoints (e.g., `pull`, `load`, etc.). - OpenAI API endpoints supported: `models`, `chat/completions` (streaming), and `embeddings`. - The `embeddings` endpoint requires embedding-specific models supported by FLM. - A single Lemonade Server process can seamlessly switch between FLM, OGA, and GGUF models. ## Installing FLM Models To install an arbitrary FLM model: 1. `flm list` to view the supported models. 1. Open the Lemonade web app by navigating to http://localhost:13305 in your web browser, click the Model Management tab, and use the Add a Model form. 1. Use the model name from `flm list` as the "checkpoint name" in the Add a Model form and select "flm" as the recipe. lemonade-sdk-lemonade-dbde812/docs/update_readme_marketplace.py000066400000000000000000000067711516551144000250200ustar00rootroot00000000000000#!/usr/bin/env python3 """ Script to update README.md with pinned apps from the marketplace. Fetches apps.json and generates icons for pinned apps. """ import json import re import sys from pathlib import Path from urllib.request import urlopen from urllib.error import URLError # Configuration APPS_JSON_URL = ( "https://raw.githubusercontent.com/lemonade-sdk/marketplace/main/apps.json" ) README_PATH = Path(__file__).parent.parent / "README.md" # Markers in README.md START_MARKER = "" END_MARKER = "" def fetch_apps() -> list: """Fetch apps from the marketplace JSON.""" try: with urlopen(APPS_JSON_URL, timeout=10) as response: data = json.loads(response.read().decode("utf-8")) return data.get("apps", []) except URLError as e: print(f"[ERROR] Failed to fetch apps.json: {e}") sys.exit(1) except json.JSONDecodeError as e: print(f"[ERROR] Failed to parse apps.json: {e}") sys.exit(1) def get_pinned_apps(apps: list, limit: int = 10) -> list: """Get pinned apps (already sorted by the marketplace build script).""" # Apps with pinned=true come first in apps.json (sorted by build.py) pinned = [app for app in apps if app.get("pinned", False)] return pinned[:limit] def generate_markdown(apps: list) -> str: """Generate markdown for pinned apps.""" if not apps: return "" # Generate icon links icons = [] for app in apps: name = app.get("name", "Unknown") logo = app.get("logo", "") link = app.get("links", {}).get("guide") or app.get("links", {}).get("app", "#") if logo: icon_html = f'{name}' icons.append(icon_html) # Join icons with spacing icons_html = "  ".join(icons) # Generate the full markdown block markdown = f"""

{icons_html}

Want your app featured here? Just submit a marketplace PR!

""" return markdown.strip() def update_readme(markdown: str) -> bool: """Update README.md with the generated markdown.""" if not README_PATH.exists(): print(f"[ERROR] README.md not found at {README_PATH}") return False content = README_PATH.read_text(encoding="utf-8") # Check if markers exist if START_MARKER not in content or END_MARKER not in content: print("[ERROR] Markers not found in README.md") print(f" Expected: {START_MARKER} ... {END_MARKER}") return False # Replace content between markers pattern = re.compile( rf"{re.escape(START_MARKER)}.*?{re.escape(END_MARKER)}", re.DOTALL ) new_content = pattern.sub(f"{START_MARKER}\n{markdown}\n{END_MARKER}", content) if new_content == content: print("[INFO] No changes needed in README.md") return True README_PATH.write_text(new_content, encoding="utf-8") print(f"[OK] Updated README.md with {len(markdown.split(' lemonade-sdk-lemonade-dbde812/examples/000077500000000000000000000000001516551144000201525ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/examples/README.md000066400000000000000000000027311516551144000214340ustar00rootroot00000000000000# Lemonade Examples Interactive demos that work with Lemonade Server. ## Available Demos ### Audio Transcription - **realtime_transcription.py**: Stream microphone audio for real-time transcription (+ test mode for WAV files) ### LLM Demos - **llm-debate.html**: A debate arena where multiple LLMs can debate each other on any topic - **multi-model-tester.html**: Test prompts across multiple models side-by-side ### Image Generation - **api_image_generation.py**: Generate images using the `images/generations` endpoint ### Speech Synthesis (TTS) - **api_text_to_speech.py**: Generate speech from a prompt using `audio/speech` endpoint ## Setup 1. Install Lemonade Server from the [latest release](https://github.com/lemonade-sdk/lemonade/releases/latest) 2. The server starts automatically after installation. 3. Pull a model if needed (e.g., `lemonade pull Whisper-Tiny`) ## Running the Examples ### Realtime Transcription (Python) Uses the OpenAI-compatible WebSocket API for real-time speech-to-text. ```bash # Install dependencies pip install openai websockets pyaudio # Stream from microphone python realtime_transcription.py # Use a different model python realtime_transcription.py --model Whisper-Small ``` ### LLM Demos Open the HTML files directly in your browser. See [debate-arena.md](debate-arena.md) for detailed instructions on the debate demo. lemonade-sdk-lemonade-dbde812/examples/api_image_edits.py000066400000000000000000000147371516551144000236430ustar00rootroot00000000000000""" This example demonstrates how to use the lemonade server API to edit images using Stable Diffusion models via the OpenAI Python client. Prerequisites: 1. Install the OpenAI client: pip install openai pillow 2. The lemonade server should be running (starts automatically after installation) 3. An image editing model will be auto-downloaded on first use 4. You need a source image to edit (example creates a simple one) Usage: python api_image_edits.py python api_image_edits.py --backend rocm python api_image_edits.py --backend cpu python api_image_edits.py --image path/to/your/image.png """ import base64 import argparse from pathlib import Path from io import BytesIO def create_sample_image(): """Create a simple sample image for testing if none provided.""" try: from PIL import Image, ImageDraw except ImportError: print("Pillow not installed. Install with: pip install pillow") return None # Create a 512x512 white image with a simple shape img = Image.new("RGB", (512, 512), color="white") draw = ImageDraw.Draw(img) # Draw a simple landscape: green ground, blue sky, yellow sun draw.rectangle([(0, 256), (512, 512)], fill="green") # Ground draw.rectangle([(0, 0), (512, 256)], fill="lightblue") # Sky draw.ellipse([(400, 50), (480, 130)], fill="yellow") # Sun output_path = Path("sample_image.png") img.save(output_path) print(f"Created sample image: {output_path.absolute()}") return output_path def edit_image_with_openai_client(image_path, backend="cpu"): """Edit an image using the OpenAI Python client.""" try: from openai import OpenAI except ImportError: print("OpenAI client not installed. Install with: pip install openai") return None # Point to local lemonade server client = OpenAI( base_url="http://localhost:13305/api/v1", api_key="not-needed", # Lemonade doesn't require API key ) print(f"Editing image with OpenAI client (backend: {backend})...") if backend == "cpu": print("(This may take several minutes with CPU backend)") # Read the image file with open(image_path, "rb") as image_file: response = client.images.edit( model="Flux-2-Klein-4B", # or another editing model image=image_file, prompt="Add a red barn and mountains in the background, photorealistic", size="512x512", n=1, ) # Save the edited image if response.data: image_data = base64.b64decode(response.data[0].b64_json) output_path = Path("edited_image_openai.png") output_path.write_bytes(image_data) print(f"Edited image saved to: {output_path.absolute()}") return output_path return None def edit_image_with_requests(image_path, backend="cpu"): """Edit an image using the requests library with multipart form data.""" try: import requests except ImportError: print("Requests not installed. Install with: pip install requests") return None print(f"Editing image with requests library (backend: {backend})...") if backend == "cpu": print("(This may take several minutes with CPU backend)") # Prepare the multipart form data with open(image_path, "rb") as image_file: files = { "image": ("image.png", image_file, "image/png"), } data = { "model": "SD-Turbo", "prompt": "Add a red barn and mountains in the background, photorealistic", "size": "512x512", "n": "1", "response_format": "b64_json", } response = requests.post( "http://localhost:13305/api/v1/images/edits", files=files, data=data, timeout=600, # 10 minutes for image generation ) if response.status_code == 200: result = response.json() if "data" in result and len(result["data"]) > 0: image_data = base64.b64decode(result["data"][0]["b64_json"]) output_path = Path("edited_image_requests.png") output_path.write_bytes(image_data) print(f"Edited image saved to: {output_path.absolute()}") return output_path else: print(f"Unexpected response format: {result}") else: print(f"Error: {response.status_code}") print(response.text) return None if __name__ == "__main__": parser = argparse.ArgumentParser( description="Edit images using Lemonade server with Stable Diffusion" ) parser.add_argument( "--backend", type=str, choices=["cpu", "rocm"], default="cpu", help="Backend to use for image editing (default: cpu). Use 'rocm' for AMD GPU acceleration.", ) parser.add_argument( "--image", type=str, help="Path to the image to edit. If not provided, a sample image will be created.", ) parser.add_argument( "--method", type=str, choices=["openai", "requests", "both"], default="both", help="Which method to use for API calls (default: both)", ) args = parser.parse_args() print("=" * 60) print("Lemonade Image Editing Example") print("=" * 60) print() print("Make sure the lemonade server is running (lemonade status)") print() # Get or create an image if args.image: image_path = Path(args.image) if not image_path.exists(): print(f"Error: Image file not found: {image_path}") exit(1) else: image_path = create_sample_image() if not image_path: exit(1) print() # Try editing with different methods results = [] if args.method in ["openai", "both"]: print("--- Using OpenAI Client ---") result = edit_image_with_openai_client(image_path, args.backend) if result: results.append(result) print() if args.method in ["requests", "both"]: print("--- Using Requests Library ---") result = edit_image_with_requests(image_path, args.backend) if result: results.append(result) print() print("=" * 60) print("Done!") if results: print("Generated images:") for result in results: print(f" - {result}") print() print("Note: The actual editing capabilities depend on the sd-cpp backend") print("and the loaded model. Some models may not support image editing yet.") lemonade-sdk-lemonade-dbde812/examples/api_image_generation.py000066400000000000000000000052121516551144000246520ustar00rootroot00000000000000""" This example demonstrates how to use the lemonade server API to generate images using Stable Diffusion models via the OpenAI Python client. Prerequisites: 1. Install the OpenAI client: pip install openai 2. The lemonade server should be running (starts automatically after installation) 3. The SD-Turbo model will be auto-downloaded on first use Usage: python api_image_generation.py python api_image_generation.py --backend rocm python api_image_generation.py --backend cpu """ import base64 import argparse from pathlib import Path def generate_with_openai_client(backend="cpu"): """Generate image using the OpenAI Python client.""" try: from openai import OpenAI except ImportError: print("OpenAI client not installed. Install with: pip install openai") return None # Point to local lemonade server client = OpenAI( base_url="http://localhost:13305/api/v1", api_key="not-needed", # Lemonade doesn't require API key ) print(f"Generating image with OpenAI client...{backend}") if backend == "cpu": print("(This may take several minutes with CPU backend)") response = client.images.generate( model="SD-Turbo", prompt="A serene mountain landscape at sunset, digital art", size="512x512", n=1, response_format="b64_json", # SD-specific parameters (passed through) extra_body={ "steps": 4, # SD-Turbo works well with 4 steps "cfg_scale": 1.0, # SD-Turbo uses low CFG }, ) # Save the image if response.data: image_data = base64.b64decode(response.data[0].b64_json) output_path = Path("generated_image_openai.png") output_path.write_bytes(image_data) print(f"Image saved to: {output_path.absolute()}") return output_path return None if __name__ == "__main__": parser = argparse.ArgumentParser( description="Generate images using Lemonade server with Stable Diffusion" ) parser.add_argument( "--backend", type=str, choices=["cpu", "rocm"], default="cpu", help="Backend to use for image generation (default: cpu). Use 'rocm' for AMD GPU acceleration.", ) args = parser.parse_args() print("=" * 60) print("Lemonade Image Generation Example") print("=" * 60) print() print("Make sure the lemonade server is running (lemonade status)") print() # Generate using OpenAI client result = generate_with_openai_client(args.backend) print() print("=" * 60) print("Done!") if result: print(f"Generated image saved to: {result}") lemonade-sdk-lemonade-dbde812/examples/api_image_variations.py000066400000000000000000000164621516551144000247070ustar00rootroot00000000000000""" This example demonstrates how to use the lemonade server API to create variations of images using Stable Diffusion models via the OpenAI Python client. Prerequisites: 1. Install the OpenAI client: pip install openai pillow 2. The lemonade server should be running (starts automatically after installation) 3. An image model will be auto-downloaded on first use 4. You need a source image (example creates a simple one) Usage: python api_image_variations.py python api_image_variations.py --backend rocm python api_image_variations.py --backend cpu python api_image_variations.py --image path/to/your/image.png python api_image_variations.py --num-variations 3 """ import base64 import argparse from pathlib import Path from io import BytesIO def create_sample_image(): """Create a simple sample image for testing if none provided.""" try: from PIL import Image, ImageDraw except ImportError: print("Pillow not installed. Install with: pip install pillow") return None # Create a 512x512 image with a simple pattern img = Image.new("RGB", (512, 512), color="white") draw = ImageDraw.Draw(img) # Draw a simple scene draw.rectangle([(0, 256), (512, 512)], fill="lightgreen") # Ground draw.rectangle([(0, 0), (512, 256)], fill="skyblue") # Sky draw.ellipse([(350, 50), (450, 150)], fill="gold") # Sun draw.rectangle([(200, 180), (300, 300)], fill="brown") # Tree trunk draw.ellipse([(150, 80), (350, 220)], fill="darkgreen") # Tree foliage output_path = Path("sample_image_variations.png") img.save(output_path) print(f"Created sample image: {output_path.absolute()}") return output_path def create_variations_with_openai_client(image_path, num_variations=1, backend="cpu"): """Create image variations using the OpenAI Python client.""" try: from openai import OpenAI except ImportError: print("OpenAI client not installed. Install with: pip install openai") return [] # Point to local lemonade server client = OpenAI( base_url="http://localhost:13305/api/v1", api_key="not-needed", # Lemonade doesn't require API key ) print( f"Creating {num_variations} variation(s) with OpenAI client (backend: {backend})..." ) if backend == "cpu": print("(This may take several minutes with CPU backend)") results = [] # Read the image file with open(image_path, "rb") as image_file: try: response = client.images.create_variation( model="SD-Turbo", image=image_file, size="512x512", n=num_variations, response_format="b64_json", ) except Exception as e: print(f"Error: {e}") return results # Save the variations if response.data: for i, image_obj in enumerate(response.data): image_data = base64.b64decode(image_obj.b64_json) output_path = Path(f"variation_openai_{i+1}.png") output_path.write_bytes(image_data) print(f"Variation {i+1} saved to: {output_path.absolute()}") results.append(output_path) return results def create_variations_with_requests(image_path, num_variations=1, backend="cpu"): """Create image variations using the requests library with multipart form data.""" try: import requests except ImportError: print("Requests not installed. Install with: pip install requests") return [] print( f"Creating {num_variations} variation(s) with requests library (backend: {backend})..." ) if backend == "cpu": print("(This may take several minutes with CPU backend)") results = [] # Prepare the multipart form data with open(image_path, "rb") as image_file: files = { "image": ("image.png", image_file, "image/png"), } data = { "model": "SD-Turbo", "size": "512x512", "n": str(num_variations), "response_format": "b64_json", } response = requests.post( "http://localhost:13305/api/v1/images/variations", files=files, data=data, timeout=600, # 10 minutes for image generation ) if response.status_code == 200: result = response.json() if "data" in result and len(result["data"]) > 0: for i, image_obj in enumerate(result["data"]): image_data = base64.b64decode(image_obj["b64_json"]) output_path = Path(f"variation_requests_{i+1}.png") output_path.write_bytes(image_data) print(f"Variation {i+1} saved to: {output_path.absolute()}") results.append(output_path) else: print(f"Unexpected response format: {result}") else: print(f"Error: {response.status_code}") print(response.text) return results if __name__ == "__main__": parser = argparse.ArgumentParser( description="Create image variations using Lemonade server with Stable Diffusion" ) parser.add_argument( "--backend", type=str, choices=["cpu", "rocm"], default="cpu", help="Backend to use for image variations (default: cpu). Use 'rocm' for AMD GPU acceleration.", ) parser.add_argument( "--image", type=str, help="Path to the source image. If not provided, a sample image will be created.", ) parser.add_argument( "--num-variations", type=int, default=1, help="Number of variations to generate (default: 1)", ) parser.add_argument( "--method", type=str, choices=["openai", "requests", "both"], default="both", help="Which method to use for API calls (default: both)", ) args = parser.parse_args() print("=" * 60) print("Lemonade Image Variations Example") print("=" * 60) print() print("Make sure the lemonade server is running (lemonade status)") print() # Get or create an image if args.image: image_path = Path(args.image) if not image_path.exists(): print(f"Error: Image file not found: {image_path}") exit(1) else: image_path = create_sample_image() if not image_path: exit(1) print() # Try creating variations with different methods all_results = [] if args.method in ["openai", "both"]: print("--- Using OpenAI Client ---") results = create_variations_with_openai_client( image_path, args.num_variations, args.backend ) all_results.extend(results) print() if args.method in ["requests", "both"]: print("--- Using Requests Library ---") results = create_variations_with_requests( image_path, args.num_variations, args.backend ) all_results.extend(results) print() print("=" * 60) print("Done!") if all_results: print(f"Generated {len(all_results)} variation(s):") for result in all_results: print(f" - {result}") print() print("Note: The actual variation capabilities depend on the sd-cpp backend") print("and the loaded model. Some models may not support image variations yet.") lemonade-sdk-lemonade-dbde812/examples/api_text_to_speech.py000066400000000000000000000032571516551144000244010ustar00rootroot00000000000000""" This example demonstrates how to use the lemonade server API to generate speech using Kokoro via the OpenAI Python client. Prerequisites: 1. Install the OpenAI client: pip install openai openai[voice_helpers] 2. The lemonade server should be running (starts automatically after installation) 3. The kokoro-v1 model will be auto-downloaded on first use Usage: python api_text_to_speech.py """ import base64 import asyncio from pathlib import Path async def generate_with_openai_client(): """Generate image using the OpenAI Python client.""" try: from openai import AsyncOpenAI from openai.helpers import LocalAudioPlayer except ImportError: print("OpenAI client not installed. Install with: pip install openai") return None # Point to local lemonade server client = AsyncOpenAI( base_url="http://localhost:13305/api/v1", api_key="not-needed", # Lemonade doesn't require API key by default ) print("Generating speech with OpenAI client...") print("(This may take several seconds)") async with client.audio.speech.with_streaming_response.create( model="kokoro-v1", voice="coral", input="Today is a wonderful day to build something people love!", stream_format="audio", ) as response: await LocalAudioPlayer().play(response) if __name__ == "__main__": print("=" * 60) print("Lemonade Text to Speech Example") print("=" * 60) print() print("Make sure the lemonade server is running (lemonade status)") print() # Generate using OpenAI client asyncio.run(generate_with_openai_client()) print() print("=" * 60) print("Done!") lemonade-sdk-lemonade-dbde812/examples/debate-arena.md000066400000000000000000000026311516551144000230060ustar00rootroot00000000000000## Debate Arena Debate Arena is a single-file HTML/CSS/JS web app that pits up to 9 LLMs against each other to answer life's most important yes/no questions. ### Quick start instructions 1. Install Lemonade Server v9.0.8 or higher: https://github.com/lemonade-sdk/lemonade/releases 2. Set the `LEMONADE_MAX_LOADED_MODELS` environment variable to `9` and restart the server (see [Server Configuration](../docs/server/configuration.md#environment-variables)) 3. Download https://github.com/lemonade-sdk/lemonade/blob/main/examples/llm-debate.html and open it in your web browser 4. You can uncheck some models to save VRAM. Running all 9 requires ~32 GB. ### How it works - All LLMs run at the same time, and the responses are streamed live to the UI. - Web app connects to Lemonade at `http://localhost:13305` - Uses the `pull` endpoint to download the models - Uses the `load` endpoint to start 9 `llama-server` background processes, one each for Qwen, Jan, LFM, etc. - The userโ€™s prompt is sent to all 9 LLMs simultaneously for a hot take by starting 9 streaming `chat/completions` requests with Lemonadeโ€™s base URL. - The request with `model=Qwen...` gets routed to Qwen's `llama-server`, and so on. - The 9 LLM responses go into a shared chat history, which is then sent back to the LLMs for reactions in debate phase 2. - Process repeats until all 9 have voted, with escalating system prompts in each phase. lemonade-sdk-lemonade-dbde812/examples/llm-debate.html000066400000000000000000002034051516551144000230520ustar00rootroot00000000000000 ๐Ÿ‹ Lemonade Debate Arena v2
โœ“ 0
โœ— 0
โณ 0
0 0 0 0 0

๐Ÿ‹ DEBATE ARENA v2

Connecting to server...
lemonade-sdk-lemonade-dbde812/examples/migrate-to-systemd.sh000077500000000000000000000111751516551144000242540ustar00rootroot00000000000000#!/bin/bash set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Check if running as root if [[ $EUID -ne 0 ]]; then echo -e "${RED}Error: This script must be run as root${NC}" echo "Please run with: sudo $0" exit 1 fi echo "Lemonade Migration Script" echo "=========================" echo "This script migrates user data from a user account to the lemonade systemd service user." echo "" # Prompt for source user read -p "Enter the username to migrate data from: " SOURCE_USER # Validate source user exists if ! id "$SOURCE_USER" &>/dev/null; then echo -e "${RED}Error: User '$SOURCE_USER' does not exist${NC}" exit 1 fi # Get source user's home directory SOURCE_HOME=$(getent passwd "$SOURCE_USER" | cut -d: -f6) if [[ -z "$SOURCE_HOME" ]]; then echo -e "${RED}Error: Could not determine home directory for user '$SOURCE_USER'${NC}" exit 1 fi if [[ ! -d "$SOURCE_HOME" ]]; then echo -e "${RED}Error: Home directory '$SOURCE_HOME' does not exist${NC}" exit 1 fi # Check if lemonade user exists if ! id "lemonade" &>/dev/null; then echo -e "${RED}Error: 'lemonade' user does not exist${NC}" echo "The systemd service user must be created before running this migration." exit 1 fi # Get lemonade user's home directory LEMONADE_HOME=$(getent passwd "lemonade" | cut -d: -f6) if [[ -z "$LEMONADE_HOME" ]]; then echo -e "${RED}Error: Could not determine home directory for user 'lemonade'${NC}" exit 1 fi if [[ ! -d "$LEMONADE_HOME" ]]; then echo -e "${RED}Error: Lemonade home directory '$LEMONADE_HOME' does not exist${NC}" exit 1 fi # Define paths to migrate declare -a SOURCE_PATHS=( "$SOURCE_HOME/.cache/lemonade" "$SOURCE_HOME/.cache/huggingface" "$SOURCE_HOME/.local/share/lemonade-server" ) declare -a DEST_PATHS=( "$LEMONADE_HOME/.cache/lemonade" "$LEMONADE_HOME/.cache/huggingface" "$LEMONADE_HOME/.local/share/lemonade-server" ) # Check which files exist echo "" echo "Scanning for files to migrate..." echo "" declare -a FILES_TO_MIGRATE=() declare -a DEST_FILES=() for i in "${!SOURCE_PATHS[@]}"; do SOURCE_PATH="${SOURCE_PATHS[$i]}" DEST_PATH="${DEST_PATHS[$i]}" if [[ -e "$SOURCE_PATH" ]]; then # Calculate size SIZE=$(du -sh "$SOURCE_PATH" 2>/dev/null | cut -f1) echo -e "${GREEN}Found:${NC} $SOURCE_PATH (${SIZE})" echo -e " -> Will migrate to: $DEST_PATH" FILES_TO_MIGRATE+=("$SOURCE_PATH") DEST_FILES+=("$DEST_PATH") fi done if [[ ${#FILES_TO_MIGRATE[@]} -eq 0 ]]; then echo -e "${YELLOW}No files found to migrate.${NC}" echo "The user may not have any lemonade data, or it may be in a custom location." exit 0 fi echo "" echo "Summary:" echo "--------" echo "Source user: $SOURCE_USER ($SOURCE_HOME)" echo "Destination user: lemonade ($LEMONADE_HOME)" echo "Files/directories to migrate: ${#FILES_TO_MIGRATE[@]}" echo "" # Confirm before proceeding read -p "Do you want to proceed with the migration? (yes/no): " CONFIRM if [[ "$CONFIRM" != "yes" ]]; then echo "Migration cancelled." exit 0 fi echo "" echo "Starting migration..." # Perform migration for i in "${!FILES_TO_MIGRATE[@]}"; do SOURCE_PATH="${FILES_TO_MIGRATE[$i]}" DEST_PATH="${DEST_FILES[$i]}" echo "" echo -e "${GREEN}Migrating:${NC} $SOURCE_PATH" echo -e " to: $DEST_PATH" # Create parent directory if needed PARENT_DIR=$(dirname "$DEST_PATH") if [[ ! -d "$PARENT_DIR" ]]; then echo " Creating parent directory: $PARENT_DIR" mkdir -p "$PARENT_DIR" chown lemonade:lemonade "$PARENT_DIR" fi # Check if destination already exists if [[ -e "$DEST_PATH" ]]; then echo -e " ${YELLOW}Warning: Destination already exists - merging files${NC}" # Use rsync to merge directories (move source files into destination) echo " Merging into existing destination..." rsync -av --remove-source-files "$SOURCE_PATH/" "$DEST_PATH/" else # Move files (preserves source structure) echo " Moving files..." mv "$SOURCE_PATH" "$DEST_PATH" fi # Update ownership echo " Updating ownership to lemonade:lemonade..." chown -R lemonade:lemonade "$DEST_PATH" echo -e " ${GREEN}Done${NC}" done echo "" echo -e "${GREEN}Migration completed successfully!${NC}" echo "" echo "Next steps:" echo "1. Verify the migrated files in $LEMONADE_HOME" echo "2. (Re)start the lemonade systemd service: systemctl restart lemonade-server" echo "3. Check service status: systemctl status lemonade-server" echo "4. Optionally, remove old files from $SOURCE_HOME to free up space" echo "" lemonade-sdk-lemonade-dbde812/examples/multi-model-tester.html000066400000000000000000001751141516551144000246050ustar00rootroot00000000000000 Lemonade Multi-Model Tester

๐Ÿ‹ Lemonade Multi-Model Tester

Test multiple models concurrently with streaming responses

Disconnected
๐ŸŽฏ

No Models Loaded

Click on a model from the sidebar to load it

lemonade-sdk-lemonade-dbde812/examples/realtime_transcription.py000066400000000000000000000224031516551144000253060ustar00rootroot00000000000000""" Realtime Audio Transcription with OpenAI SDK Uses the official OpenAI SDK to connect to Lemonade Server's OpenAI-compatible realtime transcription endpoint. Requirements: pip install openai pyaudio websockets Usage: python realtime_transcription.py python realtime_transcription.py --model Whisper-Small """ import argparse import asyncio import base64 import struct import sys import os # Enable ANSI escape codes on Windows if os.name == "nt": try: import ctypes kernel32 = ctypes.windll.kernel32 # Enable ENABLE_VIRTUAL_TERMINAL_PROCESSING kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7) except: pass # Check dependencies try: from openai import AsyncOpenAI except ImportError: print("Error: openai library not found.") print("Install it with: pip install openai") sys.exit(1) try: import pyaudio except ImportError: print("Error: pyaudio library not found.") print("Install it with: pip install pyaudio") sys.exit(1) try: import websockets # noqa: F401 โ€” required by openai SDK for realtime except ImportError: print("Error: websockets library not found.") print("Install it with: pip install websockets") sys.exit(1) TARGET_RATE = 16000 # Whisper expects 16kHz mono PCM16 CHUNK_SIZE = 4096 # Samples per read at native rate (~85ms at 48kHz) def downsample_to_16k(pcm16_bytes, native_rate): """Downsample PCM16 audio from native_rate to 16kHz using linear interpolation. Matches the resampling approach used in the Electron app's useAudioCapture hook. """ if native_rate == TARGET_RATE: return pcm16_bytes n_samples = len(pcm16_bytes) // 2 samples = struct.unpack(f"<{n_samples}h", pcm16_bytes) ratio = native_rate / TARGET_RATE output_length = int(n_samples / ratio) output = bytearray(output_length * 2) for i in range(output_length): src_idx = i * ratio idx_floor = int(src_idx) idx_ceil = min(idx_floor + 1, n_samples - 1) frac = src_idx - idx_floor sample = samples[idx_floor] * (1 - frac) + samples[idx_ceil] * frac clamped = max(-32768, min(32767, int(sample))) struct.pack_into(" term_width - 4: delta_text = "..." + delta_text[-(term_width - 4) :] print(f"\r\033[2K{delta_text}", end="", flush=True) elif ( event.type == "conversation.item.input_audio_transcription.completed" ): transcript = ( getattr(event, "transcript", "") .replace("\n", " ") .strip() ) if transcript: transcripts.append(transcript) # Clear interim line, print final on its own line print(f"\r\033[2K{transcript}") elif event.type == "error": error = getattr(event, "error", None) msg = ( getattr(error, "message", "Unknown") if error else "Unknown" ) print(f"\nError: {msg}") except asyncio.CancelledError: pass send_task = asyncio.create_task(send_audio()) recv_task = asyncio.create_task(receive_messages()) try: await asyncio.gather(send_task, recv_task) except KeyboardInterrupt: print("\n\nStopping...") send_task.cancel() recv_task.cancel() # Commit remaining audio await conn.input_audio_buffer.commit() # Wait for final transcript try: while True: event = await asyncio.wait_for(conn.recv(), timeout=3) if ( event.type == "conversation.item.input_audio_transcription.completed" ): transcript = getattr(event, "transcript", "").strip() if transcript: transcripts.append(transcript) print(f">>> {transcript}") break except: pass finally: stream.stop_stream() stream.close() pa.terminate() if transcripts: print("\n" + "-" * 40) print("Full transcript:") print(" ".join(transcripts)) asyncio.run(run()) def main(): parser = argparse.ArgumentParser( description="Realtime transcription using OpenAI-compatible API" ) parser.add_argument( "--model", default="Whisper-Tiny", help="Whisper model (default: Whisper-Tiny)" ) parser.add_argument( "--server", default="http://localhost:13305/api/v1", help="REST API URL" ) args = parser.parse_args() transcribe_microphone(args.model, args.server) if __name__ == "__main__": main() lemonade-sdk-lemonade-dbde812/mkdocs.yml000066400000000000000000000102531516551144000203400ustar00rootroot00000000000000# This is the configuration file for MkDocs, a static site generator that uses Markdown files to create documentation sites. # The configuration file is written in YAML format and contains various settings for the site. # To install the MkDocs dependencies, run the following command in the terminal: # pip install -r docs/assets/mkdocs_requirements.txt # To build the site, run the following command in the terminal: # mkdocs build # To serve the site locally, run the following command in the terminal: # mkdocs serve # To deploy the site to GitHub Pages, run the following command in the terminal: # mkdocs gh-deploy <-- this should be updated when we have CI setup with what the real instructions are. site_name: Lemonade Server Documentation site_url: https://lemonade-server.ai/ site_description: Lemonade Server is a lightweight, open-source local LLM server that allows you to run and manage multiple AI applications on your local machine. It provides a simple CLI for managing applications and supports various LLMs, making it easy to deploy and use AI models locally. edit_uri: server/README.md repo_name: lemonade-sdk/lemonade repo_url: https://github.com/lemonade-sdk/lemonade plugins: - monorepo - search theme: name: material logo: assets/logo.png # If we want to use a custom logo instead of an icon icon: repo: fontawesome/brands/github # This is the icon for the repo link in the header favicon: assets/favicon.ico features: - navigation.footer - navigation.tracking - navigation.expand - navigation.top - content.code.annotate - content.code.copy palette: # Light mode settings - scheme: lightmode primary: amber toggle: icon: material/weather-night name: Switch to dark mode # Dark mode settings - scheme: slate primary: amber accent: amber toggle: icon: material/weather-sunny name: Switch to light mode nav_style: dark # Add the list of markdown files to be included in the documentation # The order of the files in the list will determine the order they appear in the documentation nav: - Downloading and Getting Started: server/README.md - FAQ Guide: faq.md - Lemonade CLI Guide: lemonade-cli.md - Application Guides: - AI Dev Gallery: server/apps/ai-dev-gallery.md - AI Toolkit: server/apps/ai-toolkit.md - AnythingLLM: server/apps/anythingLLM.md - CodeGPT: server/apps/codeGPT.md - Continue: server/apps/continue.md - Mindcraft: server/apps/mindcraft.md - OpenHands: server/apps/open-hands.md - Open WebUI: server/apps/open-webui.md - Wut: server/apps/wut.md - Server Configuration: server/configuration.md - Understanding local LLM servers: server/concepts.md - Developer Getting Started: dev-getting-started.md - Embeddable Lemonade: - Overview: embeddable/README.md - Runtime: embeddable/runtime.md - Backends: embeddable/backends.md - Models: embeddable/models.md - Building from Source: embeddable/building.md - Server Spec: server/server_spec.md - Integration Guide: server/server_integration.md - Custom Model Configuration: server/custom-models.md - Contribution Guide: contribute.md not_in_nav: | /index.md /self_hosted_runners.md /eval/finetuned_model_export.md exclude_docs: | code.md versioning.md eval/README.md eval/humaneval_accuracy.md eval/mmlu_accuracy.md eval/perplexity.md eval/quark.md eval/ort_genai_igpu.md eval/lm-eval.md # The following adds icons on the bottom of the page extra: homepage: https://lemonade-server.ai social: - icon: simple/youtube link: https://www.youtube.com/@AMDDevCentral - icon: simple/github link: https://github.com/lemonade-sdk/lemonade - icon: simple/discord link: https://discord.gg/5xXzkMu8Zk copyright: Copyright © 2025 AMD. All rights reserved. # The custom CSS for colors and more extra_css: - assets/extra.css # The custom JavaScript for the carousel for the videos extra_javascript: - assets/carousel.js markdown_extensions: - admonition - pymdownx.superfences # Better code blocks - pymdownx.tabbed: # Tabbed code blocks alternate_style: true lemonade-sdk-lemonade-dbde812/setup.ps1000066400000000000000000000114161516551144000201240ustar00rootroot00000000000000#!/usr/bin/env pwsh # Lemonade development environment setup script for Windows # This script prepares the development environment for building Lemonade param() $ErrorActionPreference = "Stop" # Colors for output $Info = "Blue" $Success = "Green" $Warning = "Yellow" $Error = "Red" # Helper functions function Write-Info { param([string]$Message) Write-Host "[INFO] $Message" -ForegroundColor $Info } function Write-Success { param([string]$Message) Write-Host "[SUCCESS] $Message" -ForegroundColor $Success } function Write-Warning { param([string]$Message) Write-Host "[WARNING] $Message" -ForegroundColor $Warning } function Write-Error-Custom { param([string]$Message) Write-Host "[ERROR] $Message" -ForegroundColor $Error } # Check if command exists function Command-Exists { param([string]$Command) try { if (Get-Command $Command -ErrorAction Stop) { return $true } } catch { return $false } } Write-Info "Lemonade Development Setup" Write-Info "Operating System: Windows" Write-Host "" # Check and install pre-commit Write-Info "Checking pre-commit installation..." if (-not (Command-Exists "pre-commit")) { Write-Warning "pre-commit not found, installing..." if (Command-Exists "pip") { pip install pre-commit } elseif (Command-Exists "pip3") { pip3 install pre-commit } elseif (Command-Exists "py") { Write-Warning "Pip or Pip3 not found. Installing using py." py -m pip install pre-commit Write-Warning "If you encounter issues, please ensure the Python Scripts directory is in your PATH." } else { Write-Error-Custom "Neither pip nor pip3 found. Please install Python 3 first." exit 1 } Write-Success "pre-commit installed" } else { Write-Success "pre-commit is already installed" } # Install pre-commit hooks if (Test-Path ".pre-commit-config.yaml") { Write-Info "Installing pre-commit hooks..." pre-commit install Write-Success "pre-commit hooks installed" } else { Write-Warning "No .pre-commit-config.yaml found, skipping hook installation" } Write-Host "" # Step 3: Check and install Node.js and npm Write-Info "Step 3: Checking Node.js and npm installation..." if (-not (Command-Exists "node")) { Write-Error-Custom "Node.js not found" Write-Info "Please install Node.js from https://nodejs.org/" Write-Info "You can also use Chocolatey if installed: choco install nodejs" exit 1 } else { Write-Success "Node.js is installed" } if (-not (Command-Exists "npm")) { Write-Error-Custom "npm is not available" Write-Info "Please reinstall Node.js or ensure npm is in your PATH" exit 1 } else { Write-Success "npm is installed" } Write-Host "" # Check and install Node.js and npm Write-Info "Checking Node.js and npm installation..." if (-not (Command-Exists "node")) { Write-Error-Custom "Node.js not found" Write-Info "Please install Node.js from https://nodejs.org/" Write-Info "You can also use Chocolatey if installed: choco install nodejs" exit 1 } else { Write-Success "Node.js is installed" } if (-not (Command-Exists "npm")) { Write-Error-Custom "npm is not available" Write-Info "Please reinstall Node.js or ensure npm is in your PATH" exit 1 } else { Write-Success "npm is installed" } Write-Host "" # Clean and create build directory Write-Info "Preparing build directory..." if (Test-Path "build") { Write-Warning "Removing existing build directory..." Remove-Item -Recurse -Force "build" } New-Item -ItemType Directory -Path "build" -Force | Out-Null Write-Success "Build directory created" Write-Host "" # Detect Visual Studio version and select CMake preset $vswhereExe = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" $cmakePreset = "windows" if (Test-Path $vswhereExe) { $vsMajor = (& $vswhereExe -latest -property catalog_productLineVersion) if ($vsMajor -eq "18") { $cmakePreset = "vs18" } Write-Info "Detected Visual Studio v$vsMajor, using preset: $cmakePreset" } else { Write-Warning "vswhere not found, defaulting to preset: windows" } cmake --preset $cmakePreset if ($LASTEXITCODE -ne 0) { Write-Error-Custom "CMake configuration failed" exit 1 } Write-Success "CMake configured successfully" Write-Host "" Write-Host "==========================================" -ForegroundColor Green Write-Success "Setup completed successfully!" Write-Host "==========================================" -ForegroundColor Green Write-Host "" Write-Info "Next steps:" Write-Host " Build the project: cmake --build --preset windows" Write-Host " Build the electron app: cmake --build --preset windows --target electron-app" Write-Host "" Write-Info "For more information, see the README.md file" lemonade-sdk-lemonade-dbde812/setup.sh000077500000000000000000000475641516551144000200530ustar00rootroot00000000000000#!/bin/bash # Lemonade development environment setup script # This script prepares the development environment for building Lemonade set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Helper functions print_info() { echo -e "${BLUE}[INFO]${NC} $1" } print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } print_error() { echo -e "${RED}[ERROR]${NC} $1" } # Check if command exists command_exists() { command -v "$1" >/dev/null 2>&1 } # Check if running as root is_root() { [ "$(id -u)" -eq 0 ] } # Use sudo only if not root maybe_sudo() { if is_root; then "$@" else sudo "$@" fi } # Detect OS if [[ "$OSTYPE" == "linux"* ]]; then OS="linux" elif [[ "$OSTYPE" == "darwin"* ]]; then OS="macos" else print_error "Unsupported OS: $OSTYPE" exit 1 fi print_info "Lemonade Development Setup" print_info "Operating System: $OS" echo "" # Arrays to track missing dependencies missing_packages=() install_commands=() # Check pre-commit print_info "Checking pre-commit installation..." if ! command_exists pre-commit; then print_warning "pre-commit not found" if [ "$OS" = "linux" ] && command_exists pacman; then missing_packages+=("pre-commit") elif [ "$OS" = "linux" ] && command_exists apt; then missing_packages+=("pre-commit") elif [ "$OS" = "linux" ] && command_exists dnf; then missing_packages+=("pre-commit") elif [ "$OS" = "linux" ] && command_exists zypper; then missing_packages+=("python313-pre-commit") elif [ "$OS" = "macos" ] && command_exists brew; then missing_packages+=("pre-commit") elif command_exists pip || command_exists pip3; then missing_packages+=("pre-commit (via pip)") else print_error "No package manager found to install pre-commit" exit 1 fi else print_success "pre-commit is already installed" fi # Check required build tools print_info "Checking required build tools..." required_tools=("git" "cmake" "ninja" "gcc" "g++") missing_tools=() for tool in "${required_tools[@]}"; do if command_exists "$tool"; then print_success "$tool is installed" else print_warning "$tool not found" missing_tools+=("$tool") fi done if [ ${#missing_tools[@]} -gt 0 ]; then if [ "$OS" = "linux" ]; then if command_exists apt; then missing_packages+=("git" "cmake" "ninja-build" "build-essential") elif command_exists pacman; then missing_packages+=("git" "cmake" "ninja" "base-devel") elif command_exists dnf; then missing_packages+=("git" "cmake" "ninja-build" "gcc" "gcc-c++" "make") elif command_exists zypper; then missing_packages+=("git" "cmake" "ninja" "gcc" "gcc-c++" "make") fi elif [ "$OS" = "macos" ]; then missing_packages+=("git" "cmake" "ninja") fi fi # Check pkg-config and required libraries print_info "Checking pkg-config and required libraries..." if command_exists pkg-config; then print_success "pkg-config is installed" # Check for required libraries using pkg-config libs_to_check=("libcurl" "openssl" "zlib" "libsystemd" "libdrm" "libcap" "libwebsockets") missing_libs=() for lib in "${libs_to_check[@]}"; do if pkg-config --exists "$lib" 2>/dev/null; then print_success "$lib is installed" else print_warning "$lib not found" missing_libs+=("$lib") fi done if [ ${#missing_libs[@]} -gt 0 ]; then if [ "$OS" = "linux" ]; then if command_exists apt; then # Map pkg-config names to apt packages for lib in "${missing_libs[@]}"; do case "$lib" in libcurl) missing_packages+=("curl" "libcurl4-openssl-dev") ;; openssl) missing_packages+=("libssl-dev") ;; zlib) missing_packages+=("zlib1g-dev") ;; libsystemd) missing_packages+=("libsystemd-dev") ;; libdrm) missing_packages+=("libdrm-dev") ;; libcap) missing_packages+=("libcap-dev") ;; libwebsockets) missing_packages+=("libwebsockets-dev") ;; esac done elif command_exists pacman; then # Map pkg-config names to pacman packages for lib in "${missing_libs[@]}"; do case "$lib" in libcurl) missing_packages+=("curl") ;; openssl) missing_packages+=("openssl") ;; zlib) missing_packages+=("zlib") ;; libsystemd) missing_packages+=("systemd") ;; libdrm) missing_packages+=("libdrm") ;; libcap) missing_packages+=("libcap") ;; libwebsockets) ;; # Not available in Arch repos, will use FetchContent esac done elif command_exists dnf; then # Map pkg-config names to dnf packages for lib in "${missing_libs[@]}"; do case "$lib" in libcurl) missing_packages+=("libcurl-devel") ;; openssl) missing_packages+=("openssl-devel") ;; zlib) missing_packages+=("zlib-devel") ;; libsystemd) missing_packages+=("systemd-devel") ;; libdrm) missing_packages+=("libdrm-devel") ;; libcap) missing_packages+=("libcap-devel") ;; libwebsockets) missing_packages+=("libwebsockets-devel") ;; esac done elif command_exists zypper; then # Map pkg-config names to zypper packages for lib in "${missing_libs[@]}"; do case "$lib" in libcurl) missing_packages+=("libcurl-devel") ;; openssl) missing_packages+=("libopenssl-devel") ;; zlib) missing_packages+=("zlib-devel") ;; libsystemd) missing_packages+=("systemd-devel") ;; libdrm) missing_packages+=("libdrm-devel") ;; libcap) missing_packages+=("libcap-devel") ;; libwebsockets) missing_packages+=("libwebsockets-devel") ;; esac done fi elif [ "$OS" = "macos" ]; then # macOS typically has these via Xcode Command Line Tools # but can be explicitly installed via brew if needed for lib in "${missing_libs[@]}"; do case "$lib" in libcurl) missing_packages+=("curl") ;; openssl) missing_packages+=("openssl") ;; zlib) missing_packages+=("zlib") ;; esac done fi fi else print_warning "pkg-config not found, assuming libraries need to be installed" if [ "$OS" = "linux" ]; then if command_exists apt; then missing_packages+=("pkg-config" "curl" "libcurl4-openssl-dev" "libssl-dev" "zlib1g-dev" "libsystemd-dev" "libdrm-dev" "libcap-dev" "libwebsockets-dev") elif command_exists pacman; then missing_packages+=("pkgconf" "curl" "openssl" "zlib" "systemd" "libdrm" "libcap") elif command_exists dnf; then missing_packages+=("pkgconfig" "libcurl-devel" "openssl-devel" "zlib-devel" "systemd-devel" "libdrm-devel" "libcap-devel" "libwebsockets-devel") elif command_exists zypper; then missing_packages+=("pkg-config" "libcurl-devel" "libopenssl-devel" "zlib-devel" "systemd-devel" "libdrm-devel" "libcap-devel" "libwebsockets-devel") fi elif [ "$OS" = "macos" ]; then missing_packages+=("pkg-config" "curl" "openssl" "zlib" "libdrm") fi fi # Check optional Linux tray dependencies (AppIndicator3 + libnotify [+ GTK3 if not using glib variant]) # These are optional - lemonade-tray is only built when they are present. # lemonade-server always builds without them (headless, daemon-friendly). if [ "$OS" = "linux" ] && command_exists pkg-config; then print_info "Checking optional Linux tray dependencies (AppIndicator3)..." missing_tray_packages=() appindicator_glib_found=false # Check AppIndicator first โ€” the glib variant is GTK-free and preferred. if pkg-config --exists ayatana-appindicator-glib-0.1 2>/dev/null || \ pkg-config --exists ayatana-appindicator-glib 2>/dev/null; then print_success "AppIndicator3 glib variant is installed (tray support, GTK-free)" appindicator_glib_found=true elif pkg-config --exists ayatana-appindicator3-0.1 2>/dev/null || \ pkg-config --exists appindicator3-0.1 2>/dev/null; then print_success "AppIndicator3 is installed (tray support)" else print_warning "AppIndicator3 not found (optional, needed for lemonade-tray)" if command_exists apt; then missing_tray_packages+=("libayatana-appindicator3-dev") elif command_exists pacman; then missing_tray_packages+=("libayatana-appindicator") elif command_exists dnf; then missing_tray_packages+=("libayatana-appindicator-gtk3-devel") elif command_exists zypper; then missing_tray_packages+=("libayatana-appindicator3-devel") fi fi # dbusmenu-glib is required alongside the glib appindicator variant so that # GNOME Shell can find com.canonical.dbusmenu (it does not speak org.gtk.Menus). if [ "$appindicator_glib_found" = true ]; then if pkg-config --exists dbusmenu-glib-0.4 2>/dev/null; then print_success "dbusmenu-glib is installed (GNOME Shell tray menu support)" else print_warning "dbusmenu-glib not found (optional, needed for tray menus on GNOME Shell)" if command_exists apt; then missing_tray_packages+=("libdbusmenu-glib-dev") elif command_exists pacman; then missing_tray_packages+=("libdbusmenu-glib") elif command_exists dnf; then missing_tray_packages+=("dbusmenu-glib-devel") elif command_exists zypper; then missing_tray_packages+=("libdbusmenu-glib-devel") fi fi fi # GTK3 is only required when NOT using the glib appindicator variant. if [ "$appindicator_glib_found" = false ]; then if pkg-config --exists gtk+-3.0 2>/dev/null; then print_success "gtk3 is installed (tray support)" else print_warning "gtk3 not found (optional, needed for lemonade-tray)" if command_exists apt; then missing_tray_packages+=("libgtk-3-dev") elif command_exists pacman; then missing_tray_packages+=("gtk3") elif command_exists dnf; then missing_tray_packages+=("gtk3-devel") elif command_exists zypper; then missing_tray_packages+=("gtk3-devel") fi fi fi if pkg-config --exists libnotify 2>/dev/null; then print_success "libnotify is installed (tray notifications)" else print_warning "libnotify not found (optional, enables tray notifications)" if command_exists apt; then missing_tray_packages+=("libnotify-dev") elif command_exists pacman; then missing_tray_packages+=("libnotify") elif command_exists dnf; then missing_tray_packages+=("libnotify-devel") elif command_exists zypper; then missing_tray_packages+=("libnotify-devel") fi fi if [ ${#missing_tray_packages[@]} -gt 0 ]; then echo "" print_warning "Optional tray packages missing (lemonade-tray will not be built):" for pkg in "${missing_tray_packages[@]}"; do echo " - $pkg" done echo "" # Build install command for display if command_exists apt; then tray_install_cmd="sudo apt install -y ${missing_tray_packages[*]}" elif command_exists pacman; then tray_install_cmd="sudo pacman -S --needed --noconfirm ${missing_tray_packages[*]}" elif command_exists dnf; then tray_install_cmd="sudo dnf install -y ${missing_tray_packages[*]}" elif command_exists zypper; then tray_install_cmd="sudo zypper install -y ${missing_tray_packages[*]}" fi if [ -n "$tray_install_cmd" ]; then print_info "To enable tray support, run: $tray_install_cmd" else print_info "Install the packages above using your distro's package manager to enable tray support." fi if [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; then print_info "CI environment detected, skipping optional tray dependencies." else read -p "Install optional tray dependencies now? (y/N): " -n 1 -r echo "" if [[ $REPLY =~ ^[Yy]$ ]]; then print_info "Installing optional tray dependencies..." if command_exists apt; then maybe_sudo apt install -y "${missing_tray_packages[@]}" elif command_exists pacman; then maybe_sudo pacman -S --needed --noconfirm "${missing_tray_packages[@]}" elif command_exists dnf; then maybe_sudo dnf install -y "${missing_tray_packages[@]}" elif command_exists zypper; then maybe_sudo zypper install -y "${missing_tray_packages[@]}" fi print_success "Optional tray dependencies installed" else print_info "Skipping optional tray dependencies (lemonade-tray will not be built)" fi fi else print_success "All optional tray dependencies are installed (lemonade-tray will be built)" fi echo "" fi # Check Node.js and npm print_info "Checking Node.js and npm installation..." if ! command_exists node; then print_warning "Node.js not found" if [ "$OS" = "linux" ]; then if command_exists apt || command_exists pacman || command_exists dnf || command_exists zypper; then missing_packages+=("nodejs" "npm") fi elif [ "$OS" = "macos" ]; then if command_exists brew; then missing_packages+=("node") else print_error "Homebrew not found. Please install Homebrew first: https://brew.sh/" exit 1 fi fi else print_success "Node.js is already installed" fi if command_exists node && ! command_exists npm; then print_warning "npm not found" missing_packages+=("npm") fi # Check for KaTeX fonts (optional but recommended for packaging) print_info "Checking KaTeX fonts installation..." if [ "$OS" = "linux" ]; then KATEX_FONTS_DIR="/usr/share/fonts/truetype/katex" if [ -d "$KATEX_FONTS_DIR" ]; then print_success "KaTeX fonts are already installed" else print_warning "KaTeX fonts not found (optional, enables symlinks in packages)" if command_exists apt; then missing_packages+=("fonts-katex") fi fi fi echo "" # If there are missing packages, prompt for installation if [ ${#missing_packages[@]} -gt 0 ]; then print_warning "Missing dependencies detected:" for pkg in "${missing_packages[@]}"; do echo " - $pkg" done echo "" # Build installation command if [ "$OS" = "linux" ]; then if command_exists apt; then if is_root; then install_cmd="apt update && apt install -y ${missing_packages[*]}" else install_cmd="sudo apt update && sudo apt install -y ${missing_packages[*]}" fi elif command_exists pacman; then if is_root; then install_cmd="pacman -Syu --needed --noconfirm ${missing_packages[*]}" else install_cmd="sudo pacman -Syu --needed --noconfirm ${missing_packages[*]}" fi elif command_exists dnf; then if is_root; then install_cmd="dnf install -y ${missing_packages[*]}" else install_cmd="sudo dnf install -y ${missing_packages[*]}" fi elif command_exists zypper; then if is_root; then install_cmd="zypper install -y ${missing_packages[*]}" else install_cmd="sudo zypper install -y ${missing_packages[*]}" fi fi elif [ "$OS" = "macos" ]; then install_cmd="brew install ${missing_packages[*]}" fi print_info "The following command will be executed:" echo " $install_cmd" echo "" # Check if running in CI environment if [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; then print_info "CI environment detected, proceeding with automatic installation..." else read -p "Do you want to install these dependencies? (y/N): " -n 1 -r echo "" if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_error "Installation aborted by user" exit 1 fi fi print_info "Installing dependencies..." # Install based on OS and package manager if [ "$OS" = "linux" ]; then if command_exists apt; then maybe_sudo apt update # Check if pre-commit needs pip if [[ " ${missing_packages[*]} " =~ " pre-commit " ]]; then maybe_sudo apt install -y pre-commit fi # Install other packages other_packages=("${missing_packages[@]}") other_packages=("${other_packages[@]/pre-commit/}") if [ ${#other_packages[@]} -gt 0 ]; then maybe_sudo apt install -y ${other_packages[@]} fi elif command_exists pacman; then maybe_sudo pacman -Syu --needed --noconfirm ${missing_packages[@]} elif command_exists dnf; then maybe_sudo dnf install -y ${missing_packages[@]} elif command_exists zypper; then maybe_sudo zypper install -y ${missing_packages[@]} fi elif [ "$OS" = "macos" ]; then brew install ${missing_packages[@]} fi # Install pre-commit via pip if no package manager version available if [[ " ${missing_packages[*]} " =~ " pre-commit (via pip) " ]]; then if command_exists pip; then pip install pre-commit elif command_exists pip3; then pip3 install pre-commit fi fi print_success "Dependencies installed successfully" echo "" else print_success "All dependencies are already installed" echo "" fi # Install pre-commit hooks if [ -f ".pre-commit-config.yaml" ] && [ ! -f ".git/hooks/pre-commit" ] && [ -z "$CI" ] && [ -z "$GITHUB_ACTIONS" ]; then print_info "Installing pre-commit hooks..." pre-commit install print_success "pre-commit hooks installed" fi echo "" # Clean and create build directory print_info "Preparing build directory..." if [ -d "build" ]; then print_warning "Removing existing build directory..." rm -rf build fi mkdir -p build print_success "Build directory created" echo "" # Configure with CMake presets print_info "Configuring CMake with presets..." cmake --preset default print_success "CMake configured successfully" echo "" echo "==========================================" print_success "Setup completed successfully!" echo "==========================================" echo "" print_info "Next steps:" echo " Build the project: cmake --build --preset default" echo " Build the electron app: cmake --build --preset default --target electron-app" echo " Build AppImage (Linux): cmake --preset default -DBUILD_APPIMAGE=ON && cmake --build --preset default --target appimage" echo "" print_info "For more information, see the docs/dev-getting-started.md file" lemonade-sdk-lemonade-dbde812/src/000077500000000000000000000000001516551144000171235ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/src/app/000077500000000000000000000000001516551144000177035ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/src/app/.gitignore000066400000000000000000000006111516551144000216710ustar00rootroot00000000000000# Node modules node_modules/ # Build output dist/ dist-app/ build/ # OS files .DS_Store Thumbs.db # IDE .vscode/ .idea/ *.swp *.swo *~ # Logs logs/ *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Environment variables .env .env.local # Package manager # Note: package-lock.json is now tracked for reproducible CI/CD builds yarn.lock pnpm-lock.yaml # Temporary files *.tmp .temp/ lemonade-sdk-lemonade-dbde812/src/app/README.md000066400000000000000000000061221516551144000211630ustar00rootroot00000000000000# Lemonade Desktop App A desktop GUI for interacting with the Lemonade Server. ## Overview This app provides a native desktop experience for managing models and chatting with LLMs running on `lemond`. It connects to the server via HTTP API and offers a modern, resizable panel-based interface. **Key Features:** - Model management (list, pull, load/unload) - Chat interface with markdown/code rendering and LaTeX support - Real-time server log viewer - Persistent layout and inference settings - Custom frameless window with zoom controls ## Code Structure ``` src/app/ โ”œโ”€โ”€ main.js # Electron main process โ”œโ”€โ”€ preload.js # IPC bridge (contextIsolation) โ”œโ”€โ”€ webpack.config.js # Bundler config โ”œโ”€โ”€ package.json # Dependencies and build scripts โ”‚ โ”œโ”€โ”€ src/ โ”‚ โ””โ”€โ”€ renderer/ # React UI (TypeScript) โ”‚ โ”œโ”€โ”€ App.tsx # Root component, layout orchestration โ”‚ โ”œโ”€โ”€ TitleBar.tsx # Custom window controls โ”‚ โ”œโ”€โ”€ ModelManager.tsx # Model list and actions โ”‚ โ”œโ”€โ”€ ChatWindow.tsx # LLM chat interface โ”‚ โ”œโ”€โ”€ LogsWindow.tsx # Server log viewer โ”‚ โ”œโ”€โ”€ CenterPanel.tsx # Welcome/info panel โ”‚ โ”œโ”€โ”€ SettingsModal.tsx # Inference parameters โ”‚ โ””โ”€โ”€ utils/ # API helpers and config ``` ``` ## Architecture ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Electron Main Process (main.js) โ”‚ โ”‚ Window management, IPC, settings โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Preload Script (contextIsolation) โ”‚ โ”‚ Exposes safe IPC bridge to renderer โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ React Renderer (TypeScript) โ”‚ โ”‚ UI components, state management โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ HTTP API โ”‚ โ”‚ Communicates with lemond โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ## Prerequisites - **Node.js** v18 or higher - **npm** (comes with Node.js) ## Building ```bash cd src/app # Install dependencies npm install # Development (with DevTools) npm run dev # Production build npm run build ``` ## Development Scripts ```bash npm start # Build and run npm run dev # Build and run with DevTools npm run build # Production build (creates installer) npm run build:renderer # Build renderer only (webpack) npm run watch:renderer # Watch mode for renderer ``` lemonade-sdk-lemonade-dbde812/src/app/assets/000077500000000000000000000000001516551144000212055ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/src/app/assets/chevron-down-gray.svg000066400000000000000000000003531516551144000253000ustar00rootroot00000000000000 lemonade-sdk-lemonade-dbde812/src/app/assets/chevron-down-light.svg000066400000000000000000000003531516551144000254450ustar00rootroot00000000000000 lemonade-sdk-lemonade-dbde812/src/app/assets/chevron-down-white.svg000066400000000000000000000003531516551144000254560ustar00rootroot00000000000000 lemonade-sdk-lemonade-dbde812/src/app/assets/chevron-down.svg000066400000000000000000000003561516551144000243430ustar00rootroot00000000000000 lemonade-sdk-lemonade-dbde812/src/app/assets/favicon.ico000077700000000000000000000000001516551144000305352../../../docs/assets/favicon.icoustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/src/app/assets/logo.svg000066400000000000000000000114341516551144000226710ustar00rootroot00000000000000 lemonade-sdk-lemonade-dbde812/src/app/index.html000066400000000000000000000035361516551144000217070ustar00rootroot00000000000000 Lemonade App

Lemonade

Powered by Electron & Python

Welcome

This is your Lemonade with an Electron frontend and Python backend.

Backend Status

Checking connection...

Getting Started

  • The Python backend is running on port 5000
  • Click the button above to test the connection
  • Start building your lemonade features here

Lemonade App v1.0.0

lemonade-sdk-lemonade-dbde812/src/app/main.js000066400000000000000000001026671516551144000212010ustar00rootroot00000000000000const { app, BrowserWindow, ipcMain, shell, session, screen, webFrameMain, clipboard } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); const { spawn, spawnSync } = require('child_process'); const strict = require('assert/strict'); const dgram = require('dgram'); // Register lemonade:// protocol handler for deep linking from tray/CLI if (process.defaultApp) { // Dev mode: need to pass the script path app.setAsDefaultProtocolClient('lemonade', process.execPath, [path.resolve(process.argv[1])]); } else { app.setAsDefaultProtocolClient('lemonade'); } const DEFAULT_MIN_WIDTH = 400; const DEFAULT_MIN_HEIGHT = 600; const ABSOLUTE_MIN_WIDTH = 400; // Preferred initial window size to properly display center menu with both cards const PREFERRED_INITIAL_WIDTH = 1440; const PREFERRED_INITIAL_HEIGHT = 900; const MIN_ZOOM_LEVEL = -2; const MAX_ZOOM_LEVEL = 3; const ZOOM_STEP = 0.2; const BASE_SETTING_VALUES = { temperature: 0.7, topK: 40, topP: 0.9, repeatPenalty: 1.1, enableThinking: true, collapseThinkingByDefault: false, baseURL: '', apiKey: '', }; let mainWindow; let currentMinWidth = DEFAULT_MIN_WIDTH; const SETTINGS_FILE_NAME = 'app_settings.json'; const SETTINGS_UPDATED_CHANNEL = 'settings-updated'; const SERVER_PORT_UPDATED_CHANNEL = 'server-port-updated'; const CONNECTION_SETTINGS_UPDATED_CHANNEL = 'connection-settings-updated' let cachedServerPort = 13305; // Default port /** * Parse and normalize a server base URL. * - Adds http:// if no protocol specified * - Strips trailing slashes * - Returns null if URL is invalid */ const normalizeServerUrl = (url) => { if (!url || typeof url !== 'string') { return null; } let normalized = url.trim(); if (!normalized) { return null; } // Add http:// if no protocol specified if (!normalized.match(/^https?:\/\//i)) { normalized = 'http://' + normalized; } // Strip trailing slashes normalized = normalized.replace(/\/+$/, ''); // Validate URL format try { new URL(normalized); return normalized; } catch (e) { console.error('Invalid server URL:', url, e.message); return null; } }; const DEFAULT_APP_SETTINGS = Object.freeze({ temperature: { value: BASE_SETTING_VALUES.temperature, useDefault: true }, topK: { value: BASE_SETTING_VALUES.topK, useDefault: true }, topP: { value: BASE_SETTING_VALUES.topP, useDefault: true }, repeatPenalty: { value: BASE_SETTING_VALUES.repeatPenalty, useDefault: true }, enableThinking: { value: BASE_SETTING_VALUES.enableThinking, useDefault: true }, collapseThinkingByDefault: { value: BASE_SETTING_VALUES.collapseThinkingByDefault, useDefault: true }, baseURL: { value: BASE_SETTING_VALUES.baseURL, useDefault: true }, apiKey: { value: BASE_SETTING_VALUES.apiKey, useDefault: true }, }); const NUMERIC_APP_SETTING_LIMITS = Object.freeze({ temperature: { min: 0, max: 2 }, topK: { min: 1, max: 100 }, topP: { min: 0, max: 1 }, repeatPenalty: { min: 1, max: 2 }, }); const NUMERIC_APP_SETTING_KEYS = ['temperature', 'topK', 'topP', 'repeatPenalty']; const getHomeDirectory = () => { if (typeof os.homedir === 'function') { return os.homedir(); } return process.env.HOME || process.env.USERPROFILE || ''; }; const getLocalInterfaceAddresses = () => { const interfaces = os.networkInterfaces ? os.networkInterfaces() : {}; const localAddresses = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']); Object.values(interfaces || {}).forEach((entries) => { if (!Array.isArray(entries)) { return; } entries.forEach((entry) => { if (entry && typeof entry.address === 'string') { localAddresses.add(entry.address); } }); }); return localAddresses; }; const isBeaconFromLocalMachine = (address) => { if (!address || typeof address !== 'string') { return false; } const normalized = address.replace(/^::ffff:/, ''); const localAddresses = getLocalInterfaceAddresses(); return localAddresses.has(address) || localAddresses.has(normalized); }; const getCacheDirectory = () => { const homeDir = getHomeDirectory(); if (!homeDir) { return ''; } return path.join(homeDir, '.cache', 'lemonade'); }; const getAppSettingsFilePath = () => { const cacheDir = getCacheDirectory(); if (!cacheDir) { return ''; } return path.join(cacheDir, SETTINGS_FILE_NAME); }; // Default layout settings - which panels are visible and their sizes const DEFAULT_LAYOUT_SETTINGS = Object.freeze({ isChatVisible: true, isModelManagerVisible: true, isMarketplaceVisible: true, // Renamed from isCenterPanelVisible to reset user preference isLogsVisible: false, modelManagerWidth: 280, chatWidth: 350, logsHeight: 200, }); const LAYOUT_SIZE_LIMITS = Object.freeze({ modelManagerWidth: { min: 200, max: 500 }, chatWidth: { min: 250, max: 800 }, logsHeight: { min: 100, max: 400 }, }); const DEFAULT_TTS_SETTINGS = Object.freeze({ model: { value: 'kokoro-v1', useDefault: true }, userVoice: { value: 'fable', useDefault: true }, assistantVoice: { value: 'alloy', useDefault: true }, enableTTS: { value: false, useDefault: true }, enableUserTTS: { value: false, useDefault: true } }); const createDefaultAppSettings = () => ({ temperature: { ...DEFAULT_APP_SETTINGS.temperature }, topK: { ...DEFAULT_APP_SETTINGS.topK }, topP: { ...DEFAULT_APP_SETTINGS.topP }, repeatPenalty: { ...DEFAULT_APP_SETTINGS.repeatPenalty }, enableThinking: { ...DEFAULT_APP_SETTINGS.enableThinking }, collapseThinkingByDefault: { ...DEFAULT_APP_SETTINGS.collapseThinkingByDefault }, baseURL: { ...DEFAULT_APP_SETTINGS.baseURL }, apiKey: { ...DEFAULT_APP_SETTINGS.apiKey }, layout: { ...DEFAULT_LAYOUT_SETTINGS }, tts: {...DEFAULT_TTS_SETTINGS } }); const clampValue = (value, min, max) => { if (!Number.isFinite(value)) { return min; } return Math.min(Math.max(value, min), max); }; const sanitizeAppSettings = (incoming = {}) => { const sanitized = createDefaultAppSettings(); NUMERIC_APP_SETTING_KEYS.forEach((key) => { const rawSetting = incoming[key]; if (!rawSetting || typeof rawSetting !== 'object') { return; } const limits = NUMERIC_APP_SETTING_LIMITS[key]; const useDefault = typeof rawSetting.useDefault === 'boolean' ? rawSetting.useDefault : sanitized[key].useDefault; const numericValue = useDefault ? sanitized[key].value : typeof rawSetting.value === 'number' ? clampValue(rawSetting.value, limits.min, limits.max) : sanitized[key].value; sanitized[key] = { value: numericValue, useDefault, }; }); const rawEnableThinking = incoming.enableThinking; if (rawEnableThinking && typeof rawEnableThinking === 'object') { const useDefault = typeof rawEnableThinking.useDefault === 'boolean' ? rawEnableThinking.useDefault : sanitized.enableThinking.useDefault; sanitized.enableThinking = { value: useDefault ? sanitized.enableThinking.value : typeof rawEnableThinking.value === 'boolean' ? rawEnableThinking.value : sanitized.enableThinking.value, useDefault, }; } const rawCollapseThinkingByDefault = incoming.collapseThinkingByDefault; if (rawCollapseThinkingByDefault && typeof rawCollapseThinkingByDefault === 'object') { const useDefault = typeof rawCollapseThinkingByDefault.useDefault === 'boolean' ? rawCollapseThinkingByDefault.useDefault : sanitized.collapseThinkingByDefault.useDefault; sanitized.collapseThinkingByDefault = { value: useDefault ? sanitized.collapseThinkingByDefault.value : typeof rawCollapseThinkingByDefault.value === 'boolean' ? rawCollapseThinkingByDefault.value : sanitized.collapseThinkingByDefault.value, useDefault, }; } const rawBaseURL = incoming.baseURL; if (rawBaseURL && typeof rawBaseURL === 'object') { const useDefault = typeof rawBaseURL.useDefault === 'boolean' ? rawBaseURL.useDefault : sanitized.baseURL.useDefault; sanitized.baseURL = { value: useDefault ? sanitized.baseURL.value : typeof rawBaseURL.value === 'string' ? rawBaseURL.value : sanitized.baseURL.value, useDefault, }; } const rawApiKey = incoming.apiKey; if (rawApiKey && typeof rawApiKey === 'object') { const useDefault = typeof rawApiKey.useDefault === 'boolean' ? rawApiKey.useDefault : sanitized.apiKey.useDefault; sanitized.apiKey = { value: useDefault ? sanitized.apiKey.value : typeof rawApiKey.value === 'string' ? rawApiKey.value : sanitized.apiKey.value, useDefault, }; } // Sanitize layout settings const rawLayout = incoming.layout; if (rawLayout && typeof rawLayout === 'object') { // Sanitize boolean visibility settings ['isChatVisible', 'isModelManagerVisible', 'isMarketplaceVisible', 'isLogsVisible'].forEach((key) => { if (typeof rawLayout[key] === 'boolean') { sanitized.layout[key] = rawLayout[key]; } }); // Sanitize numeric size settings with limits Object.entries(LAYOUT_SIZE_LIMITS).forEach(([key, { min, max }]) => { const value = rawLayout[key]; if (typeof value === 'number' && Number.isFinite(value)) { sanitized.layout[key] = Math.min(Math.max(Math.round(value), min), max); } }); } // Sanitize TTS settings const rawTTS = incoming.tts; if (rawTTS && typeof rawTTS === 'object') { const ttsKeys = Object.keys(rawTTS); ttsKeys.forEach((key) => { if (rawTTS[key] && typeof rawTTS[key] === 'object') { const useDefault = (typeof rawTTS[key].useDefault === 'boolean') ? rawTTS[key].useDefault : sanitized.tts[key].useDefault; const value = useDefault ? sanitized.tts[key].value : (typeof rawTTS[key].value === 'string' || typeof rawTTS[key].value === 'boolean') ? rawTTS[key].value : sanitized.tts[key].value; sanitized.tts[key] = { value, useDefault }; } }); } return sanitized; }; const readAppSettingsFile = async () => { const settingsPath = getAppSettingsFilePath(); if (!settingsPath) { return createDefaultAppSettings(); } try { const content = await fs.promises.readFile(settingsPath, 'utf-8'); return sanitizeAppSettings(JSON.parse(content)); } catch (error) { if (error && error.code === 'ENOENT') { return createDefaultAppSettings(); } console.error('Failed to read app settings:', error); return createDefaultAppSettings(); } }; const writeAppSettingsFile = async (settings) => { const settingsPath = getAppSettingsFilePath(); if (!settingsPath) { throw new Error('Unable to locate the Lemonade cache-directory'); } const sanitized = sanitizeAppSettings(settings); await fs.promises.mkdir(path.dirname(settingsPath), { recursive: true }); await fs.promises.writeFile(settingsPath, JSON.stringify(sanitized, null, 2), 'utf-8'); return sanitized; }; /** * Get base URL from Settings file. * */ const getBaseURLFromConfig = async () => { const settings = await readAppSettingsFile(); const defaultBaseUrl = settings.baseURL.value; if (defaultBaseUrl) { const normalized = normalizeServerUrl(defaultBaseUrl); if (normalized) { return normalized; } return null; } }; const broadcastSettingsUpdated = (settings) => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send(SETTINGS_UPDATED_CHANNEL, settings); } }; const broadcastConnectionSettingsUpdated = (baseURL, apiKey) => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send(CONNECTION_SETTINGS_UPDATED_CHANNEL, baseURL, apiKey); } }; const broadcastServerPortUpdated = (port) => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send(SERVER_PORT_UPDATED_CHANNEL, port); } }; const fetchWithApiKey = async (entpoint) => { let serverUrl = await getBaseURLFromConfig(); let apiKey = (await readAppSettingsFile()).apiKey.value; if (!serverUrl) { serverUrl = cachedServerPort ? `http://localhost:${cachedServerPort}` : 'http://localhost:13305'; } const options = {timeout: 3000}; if(apiKey != null && apiKey != '') { options.headers = { Authorization: `Bearer ${apiKey}`, } } return await fetch(`${serverUrl}${entpoint}`, options); } const ensureTrayRunning = () => { return new Promise((resolve) => { if (process.platform !== 'darwin') { resolve(); return; } const binaryPath = '/usr/local/bin/lemonade-server'; if (!fs.existsSync(binaryPath)) { console.error(`CRITICAL: Binary not found at ${binaryPath}`); resolve(); return; } console.log('--- STARTING TRAY MANUALLY ---'); // 1. NUCLEAR CLEANUP (Crucial!) // We must kill any "Ghost" processes and delete the lock files // or the new one will think it's already running and quit immediately. try { gracefulKillBlocking('lemonade-server tray'); // Delete the lock file that cause "Already Running" error const lock = '/tmp/lemonade_Tray.lock'; if (fs.existsSync(lock)) fs.unlinkSync(lock); console.log('Cleanup complete (Zombies killed, Locks deleted).'); } catch (e) { console.error('Cleanup warning:', e.message); } // 2. PREPARE ENVIRONMENT // macOS GUI apps don't have /usr/local/bin in PATH. We must add it. const env = { ...process.env }; env.PATH = `${env.PATH}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`; // Ensure it can find libraries in /usr/local/lib env.DYLD_LIBRARY_PATH = `${env.DYLD_LIBRARY_PATH}:/usr/local/lib`; // 3. LAUNCH console.log('Spawning tray process...'); const trayProcess = spawn(binaryPath, ['tray'], { detached: true, // Allows it to run independently of the main window env: env, // Pass our fixed environment stdio: 'ignore' // Ignore output so it doesn't hang the parent }); // Unref so Electron doesn't wait for it to exit trayProcess.unref(); console.log(`Tray launched! (PID: ${trayProcess.pid})`); // Give it a moment to initialize setTimeout(resolve, 1000); }); }; function gracefulKillBlocking(processPattern) { const TIMEOUT_MS = 30000; // Send SIGTERM (Polite kill) const killResult = spawnSync('pkill', ['-f', processPattern]); // If pkill returned non-zero, the process wasn't running. We are done. if (killResult.status !== 0) { return; } // 2. Poll for exit const deadline = Date.now() + TIMEOUT_MS; let isRunning = true; while (isRunning && Date.now() < deadline) { // Check if process exists using pgrep const checkResult = spawnSync('pgrep', ['-f', processPattern]); if (checkResult.status !== 0) { // pgrep returned non-zero (process not found) -> It exited! isRunning = false; } else { // Process still exists -> Block for 1 second spawnSync('sleep', ['1']); } } // 3. Force Kill (SIGKILL) if the flag is still true if (isRunning) { spawnSync('pkill', ['-9', '-f', processPattern]); } } /** * Discovers the lemonade server using UDP broadcast beacons. * The server broadcasts its presence on UDP port 13305 with a JSON payload * containing the service name, hostname, and URL. * * This works for both local and remote servers on the same network. */ const discoverServerPort = () => { const DEFAULT_PORT = 13305; const BEACON_PORT = 13305; const DISCOVERY_TIMEOUT_MS = 5000; return new Promise((resolve) => { // Always ensure tray is running on macOS ensureTrayRunning().then(() => { const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true }); let discovered = false; let timeoutId = null; const cleanup = () => { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } try { socket.close(); } catch (e) { // Socket may already be closed } }; socket.on('error', (err) => { console.error('UDP discovery socket error:', err); cleanup(); if (!discovered) { discovered = true; const fallbackPort = cachedServerPort || DEFAULT_PORT; console.log(`Falling back to cached port due to socket error: ${fallbackPort}`); resolve(fallbackPort); } }); socket.on('message', (msg, rinfo) => { if (discovered) return; // Ignore beacons from other machines. We only derive localhost port // in discovery mode, so remote beacons can cause invalid localhost targets. if (!isBeaconFromLocalMachine(rinfo?.address)) { return; } try { const payload = JSON.parse(msg.toString()); // Verify this is a lemonade service beacon if (payload.service === 'lemonade' && payload.url) { console.log(`Discovered lemonade server via UDP beacon from ${rinfo.address}:${rinfo.port}:`, payload); // Extract port from the URL (format: http://IP:PORT/api/v1/) const urlMatch = payload.url.match(/:(\d+)\//); if (urlMatch && urlMatch[1]) { const port = parseInt(urlMatch[1], 10); if (!isNaN(port) && port > 0 && port < 65536) { discovered = true; cleanup(); console.log('Discovered server port via network beacon:', port); resolve(port); return; } } } } catch (parseError) { // Not a valid JSON beacon, ignore } }); socket.on('listening', () => { const address = socket.address(); console.log(`UDP discovery listening on ${address.address}:${address.port}`); }); // Set timeout for discovery timeoutId = setTimeout(() => { if (!discovered) { discovered = true; cleanup(); const fallbackPort = cachedServerPort || DEFAULT_PORT; console.log(`UDP discovery timeout, falling back to cached port: ${fallbackPort}`); resolve(fallbackPort); } }, DISCOVERY_TIMEOUT_MS); // Bind to the beacon port to receive broadcasts try { socket.bind(BEACON_PORT); } catch (bindError) { console.error('Failed to bind UDP socket:', bindError); cleanup(); if (!discovered) { discovered = true; resolve(cachedServerPort || DEFAULT_PORT); } } }); }); }; /** * Background beacon listener that continuously monitors for lemonade server * UDP beacons on localhost. When a beacon is received with a different port * than the currently cached port, it updates the cached port and broadcasts * the change to all renderer windows. */ let beaconSocket = null; const startBeaconListener = async () => { const BEACON_PORT = 13305; // Don't listen if an explicit base URL is configured const baseURL = await getBaseURLFromConfig(); if (baseURL) { console.log('Beacon listener skipped - using explicit server URL:', baseURL); return; } if (beaconSocket) { return; // Already listening } const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true }); beaconSocket = socket; socket.on('error', (err) => { console.error('Beacon listener socket error:', err); try { socket.close(); } catch (e) { /* ignore */ } beaconSocket = null; // Retry after 10 seconds setTimeout(() => startBeaconListener(), 10000); }); socket.on('message', (msg, rinfo) => { try { if (!isBeaconFromLocalMachine(rinfo?.address)) { return; } const payload = JSON.parse(msg.toString()); if (payload.service === 'lemonade' && payload.url) { const urlMatch = payload.url.match(/:(\d+)\//); if (urlMatch && urlMatch[1]) { const port = parseInt(urlMatch[1], 10); if (!isNaN(port) && port > 0 && port < 65536 && port !== cachedServerPort) { console.log(`Beacon listener detected server port change: ${cachedServerPort} -> ${port}`); cachedServerPort = port; broadcastServerPortUpdated(port); } } } } catch (e) { // Not a valid JSON beacon, ignore } }); socket.on('listening', () => { const address = socket.address(); console.log(`Beacon listener started on ${address.address}:${address.port}`); }); try { socket.bind(BEACON_PORT); } catch (bindError) { console.error('Failed to bind beacon listener socket:', bindError); beaconSocket = null; // Retry after 10 seconds setTimeout(() => startBeaconListener(), 10000); } }; // Renderer signals that React has mounted and IPC listeners are active. // Deliver any pending lemonade:// protocol navigation now. ipcMain.on('renderer-ready', () => { rendererReady = true; if (pendingProtocolNav && Object.keys(pendingProtocolNav).length > 0) { if (mainWindow && mainWindow.webContents) { mainWindow.webContents.send('navigate', pendingProtocolNav); } pendingProtocolNav = null; } }); ipcMain.handle('write-clipboard', (_event, text) => { clipboard.writeText(String(text)); }); // Returns the configured server base URL, or null if using localhost discovery ipcMain.handle('get-server-base-url', async () => { return await getBaseURLFromConfig(); }); ipcMain.handle('get-server-api-key', async () => { return (await readAppSettingsFile()).apiKey.value; }); ipcMain.handle('get-app-settings', async () => { return readAppSettingsFile(); }); ipcMain.handle('save-app-settings', async (_event, payload) => { const sanitized = await writeAppSettingsFile(payload); broadcastSettingsUpdated(sanitized); broadcastConnectionSettingsUpdated(sanitized.baseURL.value, sanitized.apiKey.value); return sanitized; }); ipcMain.handle('get-version', async () => { try { const response = await fetchWithApiKey('/api/v1/health'); if(response.ok) { const data = await response.json(); return data.version || 'Unknown'; } } catch(error) { console.error('Failed to fetch version from server:', error); return 'Unknown'; } }); ipcMain.handle('discover-server-port', async () => { let baseURL = await getBaseURLFromConfig(); if (baseURL) { console.log('Port discovery skipped - using explicit server URL:', baseURL); ensureTrayRunning(); return null; } const port = await discoverServerPort(); cachedServerPort = port; broadcastServerPortUpdated(port); return port; }); ipcMain.handle('get-server-port', async () => { return cachedServerPort; }); ipcMain.handle('get-system-stats', async () => { try { const response = await fetchWithApiKey('/api/v1/system-stats'); if (response.ok) { const data = await response.json(); return { cpu_percent: data.cpu_percent, memory_gb: data.memory_gb || 0, gpu_percent: data.gpu_percent, vram_gb: data.vram_gb, npu_percent: data.npu_percent, }; } } catch (error) { console.error('Failed to fetch system stats from server:', error); } // Return null stats if server is unavailable return { cpu_percent: null, memory_gb: 0, gpu_percent: null, vram_gb: null, npu_percent: null, }; }); ipcMain.handle('get-system-info', async () => { try { const response = await fetchWithApiKey('/api/v1/system-info'); if (response.ok) { const data = await response.json(); let maxGttGb = 0; let maxVramGb = 0; const considerAmdGpu = (gpu) => { if (gpu && typeof gpu.virtual_mem_gb === 'number' && isFinite(gpu.virtual_mem_gb)) { maxGttGb = Math.max(maxGttGb, gpu.virtual_mem_gb); } if (gpu && typeof gpu.vram_gb === 'number' && isFinite(gpu.vram_gb)) { maxVramGb = Math.max(maxVramGb, gpu.vram_gb); } }; if (data.devices?.amd_igpu) { considerAmdGpu(data.devices.amd_igpu); } if (Array.isArray(data.devices?.amd_dgpu)) { data.devices.amd_dgpu.forEach(considerAmdGpu); } // Transform server response to match the About window format const systemInfo = { system: 'Unknown', os: data['OS Version'] || 'Unknown', cpu: data['Processor'] || 'Unknown', gpus: [], gtt_gb: maxGttGb > 0 ? `${maxGttGb} GB` : undefined, vram_gb: maxVramGb > 0 ? `${maxVramGb} GB` : undefined, }; // Extract GPU information from devices if (data.devices) { if (data.devices.amd_igpu?.name) { systemInfo.gpus.push(data.devices.amd_igpu.name); } if (data.devices.nvidia_igpu?.name) { systemInfo.gpus.push(data.devices.nvidia_igpu.name); } if (Array.isArray(data.devices.amd_dgpu)) { data.devices.amd_dgpu.forEach(gpu => { if (gpu.name) systemInfo.gpus.push(gpu.name); }); } if (Array.isArray(data.devices.nvidia_dgpu)) { data.devices.nvidia_dgpu.forEach(gpu => { if (gpu.name) systemInfo.gpus.push(gpu.name); }); } } return systemInfo; } } catch (error) { console.error('Failed to fetch system info from server:', error); } // Return default if server is unavailable return { system: 'Unknown', os: 'Unknown', cpu: 'Unknown', gpus: [], gtt_gb: 'Unknown', vram_gb: 'Unknown', }; }); // Get local marketplace file URL for development fallback ipcMain.handle('get-local-marketplace-url', async () => { // In development, the marketplace.html is in the docs folder at project root // In production, it would be bundled differently const docsPath = app.isPackaged ? path.join(process.resourcesPath, 'docs', 'marketplace.html') : path.join(__dirname, '..', '..', 'docs', 'marketplace.html'); // Check if file exists if (fs.existsSync(docsPath)) { return `file://${docsPath}?embedded=true&theme=dark`; } return null; }); function updateWindowMinWidth(requestedWidth) { if (!mainWindow || typeof requestedWidth !== 'number' || !isFinite(requestedWidth)) { return; } const safeWidth = Math.max(Math.round(requestedWidth), ABSOLUTE_MIN_WIDTH); if (safeWidth === currentMinWidth) { return; } currentMinWidth = safeWidth; mainWindow.setMinimumSize(currentMinWidth, DEFAULT_MIN_HEIGHT); } const clampZoomLevel = (level) => { return Math.min(Math.max(level, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL); }; const adjustZoomLevel = (delta) => { if (!mainWindow || mainWindow.isDestroyed()) { return; } const currentLevel = mainWindow.webContents.getZoomLevel(); const nextLevel = clampZoomLevel(currentLevel + delta); if (nextLevel !== currentLevel) { mainWindow.webContents.setZoomLevel(nextLevel); } }; function createWindow() { // Get the primary display's work area (excludes taskbar/dock) const primaryDisplay = screen.getPrimaryDisplay(); const { width: screenWidth, height: screenHeight } = primaryDisplay.workAreaSize; // Use preferred size or 90% of screen size, whichever is smaller const initialWidth = Math.min(PREFERRED_INITIAL_WIDTH, Math.floor(screenWidth * 0.9)); const initialHeight = Math.min(PREFERRED_INITIAL_HEIGHT, Math.floor(screenHeight * 0.9)); mainWindow = new BrowserWindow({ width: initialWidth, height: initialHeight, minWidth: DEFAULT_MIN_WIDTH, minHeight: DEFAULT_MIN_HEIGHT, backgroundColor: '#000000', frame: false, icon: path.join(__dirname, '..', '..', 'docs', 'assets', 'favicon.ico'), webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }); // In development, load from dist/renderer; in production from root const htmlPath = app.isPackaged ? path.join(__dirname, 'dist', 'renderer', 'index.html') : path.join(__dirname, 'dist', 'renderer', 'index.html'); mainWindow.loadFile(htmlPath); // Pending lemonade:// navigation is delivered when the renderer signals ready // via the 'renderer-ready' IPC (see ipcMain.on below), not on did-finish-load, // because React effects haven't registered their listeners at that point. // Open all external links in the default browser mainWindow.webContents.setWindowOpenHandler(({ url }) => { // Open in external browser instead of new Electron window shell.openExternal(url); return { action: 'deny' }; // Prevent Electron from opening a new window }); const isHttpUrl = (value) => { try { const parsed = new URL(value); return parsed.protocol === 'http:' || parsed.protocol === 'https:'; } catch { return false; } }; // Intercept all anchor clicks inside loaded iframe pages and route them through // window.open so Electron's setWindowOpenHandler opens them in the system browser. const injectIframeLinkInterceptor = (frame) => { if (!frame || !isHttpUrl(frame.url) || frame === mainWindow.webContents.mainFrame) { return; } frame.executeJavaScript(` (() => { if (window.__lemonadeExternalLinkInterceptorInstalled) return; window.__lemonadeExternalLinkInterceptorInstalled = true; document.addEventListener('click', (event) => { const element = event.target && event.target.closest ? event.target.closest('a[href]') : null; if (!element) return; const href = element.href || element.getAttribute('href') || ''; if (!/^https?:\\/\\//i.test(href)) return; event.preventDefault(); event.stopPropagation(); window.open(href, '_blank', 'noopener,noreferrer'); }, true); })(); `).catch(() => {}); }; mainWindow.webContents.on('did-frame-finish-load', (_event, isMainFrame, frameProcessId, frameRoutingId) => { if (isMainFrame) { return; } const frame = webFrameMain.fromId(frameProcessId, frameRoutingId); injectIframeLinkInterceptor(frame); }); // Open DevTools in development mode if (!app.isPackaged) { mainWindow.webContents.openDevTools(); } // Listen for maximize/unmaximize events mainWindow.on('maximize', () => { mainWindow.webContents.send('maximize-change', true); }); mainWindow.on('unmaximize', () => { mainWindow.webContents.send('maximize-change', false); }); mainWindow.on('closed', function () { mainWindow = null; rendererReady = false; }); } // Pending protocol navigation โ€” stored when URL arrives before renderer is ready let pendingProtocolNav = null; let rendererReady = false; function handleProtocolUrl(url) { if (!url || !url.startsWith('lemonade://')) return; try { // Parse: lemonade://open?view=logs&model=foo const parsed = new URL(url); const view = parsed.searchParams.get('view'); const model = parsed.searchParams.get('model'); const navData = {}; if (view) navData.view = view; if (model) navData.model = model; // Focus the window if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.show(); mainWindow.focus(); } // If renderer isn't ready yet, queue it for delivery after load if (!rendererReady || !mainWindow || !mainWindow.webContents) { pendingProtocolNav = navData; return; } if (Object.keys(navData).length > 0) { mainWindow.webContents.send('navigate', navData); } } catch (e) { console.error('Failed to parse protocol URL:', url, e); } } const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); } else { app.on('second-instance', (event, commandLine, workingDirectory) => { // Someone tried to run a second instance -- focus existing window if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } // On Windows, the protocol URL is in commandLine const url = commandLine.find(arg => arg.startsWith('lemonade://')); if (url) { handleProtocolUrl(url); } }); } app.on('open-url', (event, url) => { event.preventDefault(); handleProtocolUrl(url); }); app.on('ready', () => { ensureTrayRunning(); startBeaconListener(); createWindow(); // Handle protocol URL from initial launch (Windows/Linux) const protocolUrl = process.argv.find(arg => arg.startsWith('lemonade://')); if (protocolUrl) { handleProtocolUrl(protocolUrl); } // Allow microphone access for streaming audio transcription. // Only 'media' is auto-approved; all other permissions use Electron's default (deny). session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => { callback(permission === 'media'); }); // Window control handlers ipcMain.on('minimize-window', () => { if (mainWindow) mainWindow.minimize(); }); ipcMain.on('maximize-window', () => { if (mainWindow) { if (mainWindow.isMaximized()) { mainWindow.unmaximize(); } else { mainWindow.maximize(); } } }); ipcMain.on('close-window', () => { if (mainWindow) mainWindow.close(); }); ipcMain.on('open-external', (event, url) => { shell.openExternal(url); }); ipcMain.on('update-min-width', (_event, width) => { updateWindowMinWidth(width); }); ipcMain.on('zoom-in', () => { adjustZoomLevel(ZOOM_STEP); }); ipcMain.on('zoom-out', () => { adjustZoomLevel(-ZOOM_STEP); }); }); app.on('window-all-closed', function () { if (process.platform !== 'darwin') { app.quit(); } }); app.on('will-quit', () => { if (beaconSocket) { try { beaconSocket.close(); } catch (e) { /* ignore */ } beaconSocket = null; } }); app.on('activate', function () { if (mainWindow === null) { createWindow(); } }); lemonade-sdk-lemonade-dbde812/src/app/package-lock.json000066400000000000000000012657541516551144000231430ustar00rootroot00000000000000{ "name": "lemonade", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lemonade", "version": "1.0.0", "license": "MIT", "dependencies": { "@types/markdown-it": "^14.1.2", "highlight.js": "^11.11.1", "katex": "^0.16.25", "markdown-it": "^14.1.0", "markdown-it-texmath": "^1.0.0", "react": "^19.2.0", "react-dom": "^19.2.0" }, "devDependencies": { "@types/katex": "^0.16.7", "@types/node": "^20.10.5", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "css-loader": "^7.1.4", "electron": "^39.2.4", "electron-builder": "^26.8.1", "glob": ">=10.5.0", "html-webpack-plugin": "^5.6.6", "node-forge": ">=1.3.2", "style-loader": "^4.0.0", "ts-loader": "^9.5.4", "typescript": "^5.3.3", "webpack": "^5.105.3", "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.2.3" } }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" }, "engines": { "node": ">= 8.9.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" } }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", "dev": true, "license": "MIT", "engines": { "node": ">=14.17.0" } }, "node_modules/@electron/asar": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", "dev": true, "license": "MIT", "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" }, "engines": { "node": ">=10.12.0" } }, "node_modules/@electron/asar/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/@electron/asar/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/@electron/asar/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@electron/asar/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/@electron/fuses": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "node_modules/@electron/fuses/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=10" } }, "node_modules/@electron/fuses/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/@electron/fuses/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "engines": { "node": ">=12" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "node_modules/@electron/notarize": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" }, "engines": { "node": ">= 10.0.0" } }, "node_modules/@electron/notarize/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=10" } }, "node_modules/@electron/notarize/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/@electron/notarize/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/@electron/osx-sign": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" }, "engines": { "node": ">=12.0.0" } }, "node_modules/@electron/osx-sign/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=12" } }, "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, "license": "MIT", "engines": { "node": ">= 8.0.0" }, "funding": { "url": "https://github.com/sponsors/gjtorikian/" } }, "node_modules/@electron/osx-sign/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/@electron/osx-sign/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/@electron/rebuild": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", "dev": true, "license": "MIT", "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@electron/rebuild/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/@electron/universal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", "dev": true, "license": "MIT", "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" }, "engines": { "node": ">=16.4" } }, "node_modules/@electron/universal/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/@electron/universal/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@electron/universal/node_modules/fs-extra": { "version": "11.3.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=14.14" } }, "node_modules/@electron/universal/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/@electron/universal/node_modules/minimatch": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@electron/universal/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/@electron/windows-sign": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", "dev": true, "license": "BSD-2-Clause", "optional": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" }, "engines": { "node": ">=14.14" } }, "node_modules/@electron/windows-sign/node_modules/fs-extra": { "version": "11.3.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=14.14" } }, "node_modules/@electron/windows-sign/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/@electron/windows-sign/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "optional": true, "engines": { "node": ">= 10.0.0" } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { "node": ">=12" } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" }, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" }, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { "version": "0.3.11", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@jsonjoy.com/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/buffers": { "version": "17.67.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/codegen": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-core": { "version": "4.56.10", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.10.tgz", "integrity": "sha512-PyAEA/3cnHhsGcdY+AmIU+ZPqTuZkDhCXQ2wkXypdLitSpd6d5Ivxhnq4wa2ETRWFVJGabYynBWxIijOswSmOw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.56.10", "@jsonjoy.com/fs-node-utils": "4.56.10", "thingies": "^2.5.0" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-fsa": { "version": "4.56.10", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.10.tgz", "integrity": "sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/fs-core": "4.56.10", "@jsonjoy.com/fs-node-builtins": "4.56.10", "@jsonjoy.com/fs-node-utils": "4.56.10", "thingies": "^2.5.0" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-node": { "version": "4.56.10", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.10.tgz", "integrity": "sha512-7R4Gv3tkUdW3dXfXiOkqxkElxKNVdd8BDOWC0/dbERd0pXpPY+s2s1Mino+aTvkGrFPiY+mmVxA7zhskm4Ue4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/fs-core": "4.56.10", "@jsonjoy.com/fs-node-builtins": "4.56.10", "@jsonjoy.com/fs-node-utils": "4.56.10", "@jsonjoy.com/fs-print": "4.56.10", "@jsonjoy.com/fs-snapshot": "4.56.10", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-node-builtins": { "version": "4.56.10", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.10.tgz", "integrity": "sha512-uUnKz8R0YJyKq5jXpZtkGV9U0pJDt8hmYcLRrPjROheIfjMXsz82kXMgAA/qNg0wrZ1Kv+hrg7azqEZx6XZCVw==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-node-to-fsa": { "version": "4.56.10", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.10.tgz", "integrity": "sha512-oH+O6Y4lhn9NyG6aEoFwIBNKZeYy66toP5LJcDOMBgL99BKQMUf/zWJspdRhMdn/3hbzQsZ8EHHsuekbFLGUWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/fs-fsa": "4.56.10", "@jsonjoy.com/fs-node-builtins": "4.56.10", "@jsonjoy.com/fs-node-utils": "4.56.10" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-node-utils": { "version": "4.56.10", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.10.tgz", "integrity": "sha512-8EuPBgVI2aDPwFdaNQeNpHsyqPi3rr+85tMNG/lHvQLiVjzoZsvxA//Xd8aB567LUhy4QS03ptT+unkD/DIsNg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/fs-node-builtins": "4.56.10" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-print": { "version": "4.56.10", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.10.tgz", "integrity": "sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/fs-node-utils": "4.56.10", "tree-dump": "^1.1.0" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-snapshot": { "version": "4.56.10", "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.10.tgz", "integrity": "sha512-DkR6l5fj7+qj0+fVKm/OOXMGfDFCGXLfyHkORH3DF8hxkpDgIHbhf/DwncBMs2igu/ST7OEkexn1gIqoU6Y+9g==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", "@jsonjoy.com/fs-node-utils": "4.56.10", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { "version": "17.67.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { "version": "17.67.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { "version": "17.67.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/base64": "17.67.0", "@jsonjoy.com/buffers": "17.67.0", "@jsonjoy.com/codegen": "17.67.0", "@jsonjoy.com/json-pointer": "17.67.0", "@jsonjoy.com/util": "17.67.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { "version": "17.67.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/util": "17.67.0" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { "version": "17.67.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/buffers": "17.67.0", "@jsonjoy.com/codegen": "17.67.0" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/json-pack": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/base64": "^1.1.2", "@jsonjoy.com/buffers": "^1.2.0", "@jsonjoy.com/codegen": "^1.0.0", "@jsonjoy.com/json-pointer": "^1.0.2", "@jsonjoy.com/util": "^1.9.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/json-pointer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/codegen": "^1.0.0", "@jsonjoy.com/util": "^1.9.0" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/util": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/buffers": "^1.0.0", "@jsonjoy.com/codegen": "^1.0.0" }, "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true, "license": "MIT" }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", "dev": true, "funding": [ { "type": "individual", "url": "https://github.com/sponsors/malept" }, { "type": "tidelift", "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" } ], "license": "Apache-2.0", "dependencies": { "cross-spawn": "^7.0.1" }, "engines": { "node": ">= 12.13.0" } }, "node_modules/@malept/flatpak-bundler": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" }, "engines": { "node": ">= 10.0.0" } }, "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=10" } }, "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/@malept/flatpak-bundler/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true, "license": "MIT", "engines": { "node": ">= 16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@npmcli/agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", "dev": true, "license": "ISC", "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/agent/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/@npmcli/fs": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", "dev": true, "license": "ISC", "dependencies": { "semver": "^7.3.5" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/fs/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/@peculiar/asn1-cms": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509-attr": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-csr": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-ecc": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pfx": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-cms": "^2.6.1", "@peculiar/asn1-pkcs8": "^2.6.1", "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pkcs8": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pkcs9": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-cms": "^2.6.1", "@peculiar/asn1-pfx": "^2.6.1", "@peculiar/asn1-pkcs8": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509-attr": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-rsa": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-schema": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", "dev": true, "license": "MIT", "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-x509": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-x509-attr": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/x509": { "version": "1.14.3", "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "license": "MIT", "optional": true, "engines": { "node": ">=14" } }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sindresorhus/is?sponsor=1" } }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "dev": true, "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.0" }, "engines": { "node": ">=10" } }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "node_modules/@types/bonjour": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dev": true, "license": "MIT", "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" } }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*" } }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "node_modules/@types/eslint-scope": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/express": { "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { "version": "4.19.8", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", "dev": true, "license": "MIT" }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "dev": true, "license": "MIT" }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "dev": true, "license": "MIT" }, "node_modules/@types/http-proxy": { "version": "1.17.17", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/katex": { "version": "0.16.7", "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", "dev": true, "license": "MIT" }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", "license": "MIT" }, "node_modules/@types/markdown-it": { "version": "14.1.2", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "license": "MIT", "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "node_modules/@types/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true, "license": "MIT" }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "20.19.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz", "integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "dev": true, "license": "MIT" }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/serve-index": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, "license": "MIT", "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { "version": "1.15.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "node_modules/@types/serve-static/node_modules/@types/send": { "version": "0.17.6", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", "dev": true, "license": "MIT", "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/verror": { "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", "dev": true, "license": "MIT", "optional": true }, "node_modules/@types/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/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/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "node_modules/@webpack-cli/configtest": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", "dev": true, "license": "MIT", "engines": { "node": ">=18.12.0" }, "peerDependencies": { "webpack": "^5.82.0", "webpack-cli": "6.x.x" } }, "node_modules/@webpack-cli/info": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", "dev": true, "license": "MIT", "engines": { "node": ">=18.12.0" }, "peerDependencies": { "webpack": "^5.82.0", "webpack-cli": "6.x.x" } }, "node_modules/@webpack-cli/serve": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", "dev": true, "license": "MIT", "engines": { "node": ">=18.12.0" }, "peerDependencies": { "webpack": "^5.82.0", "webpack-cli": "6.x.x" }, "peerDependenciesMeta": { "webpack-dev-server": { "optional": true } } }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, "license": "Apache-2.0" }, "node_modules/7zip-bin": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "dev": true, "license": "MIT" }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", "dev": true, "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" }, "engines": { "node": ">= 0.6" } }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "peer": true, "bin": { "acorn": "bin/acorn" }, "engines": { "node": ">=0.4.0" } }, "node_modules/acorn-import-phases": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" }, "peerDependencies": { "acorn": "^8.14.0" } }, "node_modules/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/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ajv-formats": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" }, "peerDependenciesMeta": { "ajv": { "optional": true } } }, "node_modules/ajv-formats/node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ajv-formats/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } }, "node_modules/ansi-html-community": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", "dev": true, "engines": [ "node >= 0.8.0" ], "license": "Apache-2.0", "bin": { "ansi-html": "bin/ansi-html" } }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" }, "engines": { "node": ">= 8" } }, "node_modules/app-builder-bin": { "version": "5.0.0-alpha.12", "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", "dev": true, "license": "MIT" }, "node_modules/app-builder-lib": { "version": "26.8.1", "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", "dev": true, "license": "MIT", "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "node_modules/app-builder-lib/node_modules/@electron/get": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "engines": { "node": ">=14" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" }, "engines": { "node": ">=6 <7 || >=8" } }, "node_modules/app-builder-lib/node_modules/@electron/get/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/app-builder-lib/node_modules/ci-info": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/sibiraj-s" } ], "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/app-builder-lib/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=12" } }, "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/app-builder-lib/node_modules/isexe": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, "node_modules/app-builder-lib/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/app-builder-lib/node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true, "license": "MIT" }, "node_modules/asn1js": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" }, "engines": { "node": ">=12.0.0" } }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true, "license": "MIT", "optional": true, "engines": { "node": ">=0.8" } }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", "optional": true, "engines": { "node": ">=8" } }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true, "license": "MIT" }, "node_modules/async-exit-hook": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, "license": "ISC", "engines": { "node": ">= 4.0.0" } }, "node_modules/balanced-match": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", "dev": true, "license": "MIT", "engines": { "node": "20 || >=22" } }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "license": "MIT" }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" }, "engines": { "node": ">=6.0.0" } }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true, "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/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/body-parser/node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, "node_modules/body-parser/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, "engines": { "node": ">=0.10.0" } }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/body-parser/node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/bonjour-service": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "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/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, "license": "MIT", "optional": true }, "node_modules/brace-expansion": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { "node": "20 || >=22" } }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" }, "engines": { "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "license": "MIT", "engines": { "node": "*" } }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, "node_modules/builder-util": { "version": "26.8.1", "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", "dev": true, "license": "MIT", "dependencies": { "@types/debug": "^4.1.6", "7zip-bin": "~5.2.0", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "node_modules/builder-util-runtime": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" }, "engines": { "node": ">=12.0.0" } }, "node_modules/builder-util/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=12" } }, "node_modules/builder-util/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/builder-util/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" }, "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/bytestreamjs": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=6.0.0" } }, "node_modules/cacache": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", "dev": true, "license": "ISC", "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/cacache/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/cacache/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/cacache/node_modules/glob": { "version": "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/cacache/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/cacache/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/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "dev": true, "license": "MIT", "engines": { "node": ">=10.6.0" } }, "node_modules/cacheable-request": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dev": true, "license": "MIT", "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" }, "engines": { "node": ">=8" } }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/call-bound": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/camel-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", "dev": true, "license": "MIT", "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" } }, "node_modules/caniuse-lite": { "version": "1.0.30001775", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz", "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/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/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6.0" } }, "node_modules/chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", "dev": true, "license": "MIT" }, "node_modules/ci-info": { "version": "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/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dev": true, "license": "MIT", "dependencies": { "source-map": "~0.6.0" }, "engines": { "node": ">= 10.0" } }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "license": "MIT", "dependencies": { "restore-cursor": "^3.1.0" }, "engines": { "node": ">=8" } }, "node_modules/cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cli-truncate": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" }, "engines": { "node": ">=12" } }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "dev": true, "license": "MIT", "engines": { "node": ">=0.8" } }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, "engines": { "node": ">=7.0.0" } }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, "node_modules/commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "dev": true, "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/compare-version": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, "engines": { "node": ">= 0.6" } }, "node_modules/compression": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/compression/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/compression/node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/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/connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.8" } }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, "engines": { "node": ">= 0.6" } }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true, "license": "MIT" }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true, "license": "MIT" }, "node_modules/crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "buffer": "^5.1.0" } }, "node_modules/cross-dirname": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" }, "engines": { "node": ">= 8" } }, "node_modules/css-loader": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", "dev": true, "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.40", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.6.3" }, "engines": { "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "webpack": "^5.27.0" }, "peerDependenciesMeta": { "@rspack/core": { "optional": true }, "webpack": { "optional": true } } }, "node_modules/css-loader/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/css-select": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" }, "funding": { "url": "https://github.com/sponsors/fb55" } }, "node_modules/css-what": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, "funding": { "url": "https://github.com/sponsors/fb55" } }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, "engines": { "node": ">=4" } }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" }, "engines": { "node": ">=6.0" }, "peerDependenciesMeta": { "supports-color": { "optional": true } } }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/default-browser": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" }, "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/default-browser-id": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "dev": true, "license": "MIT", "dependencies": { "clone": "^1.0.2" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true, "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" } }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true, "license": "MIT" }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", "dev": true, "license": "MIT", "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "node_modules/dir-compare/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/dir-compare/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/dir-compare/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/dir-compare/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/dmg-builder": { "version": "26.8.1", "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "node_modules/dmg-builder/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=12" } }, "node_modules/dmg-builder/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/dmg-builder/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/dmg-license": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "bin": { "dmg-license": "bin/dmg-license.js" }, "engines": { "node": ">=8" } }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, "engines": { "node": ">=6" } }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", "dev": true, "license": "MIT", "dependencies": { "utila": "~0.4" } }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" }, "funding": { "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/fb55" } ], "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" }, "engines": { "node": ">= 4" }, "funding": { "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" } }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "dev": true, "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" }, "funding": { "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { "version": "11.0.7", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "dotenv": "^16.4.5" }, "engines": { "node": ">=12" }, "funding": { "url": "https://dotenvx.com" } }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, "license": "MIT" }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" }, "engines": { "node": ">=0.10.0" } }, "node_modules/electron": { "version": "39.2.4", "resolved": "https://registry.npmjs.org/electron/-/electron-39.2.4.tgz", "integrity": "sha512-KxPtwpFceQKSxRtUY39piHLYhJMMyHfOhc70e6zRnKGrbRdK6hzEqssth8IGjlKOdkeT4KCvIEngnNraYk39+g==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" }, "engines": { "node": ">= 12.20.55" } }, "node_modules/electron-builder": { "version": "26.8.1", "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", "dev": true, "license": "MIT", "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" }, "engines": { "node": ">=14.0.0" } }, "node_modules/electron-builder-squirrel-windows": { "version": "26.8.1", "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "node_modules/electron-builder/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=12" } }, "node_modules/electron-builder/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/electron-builder/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/electron-publish": { "version": "26.8.1", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", "dev": true, "license": "MIT", "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "node_modules/electron-publish/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=12" } }, "node_modules/electron-publish/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/electron-publish/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/electron-to-chromium": { "version": "1.5.302", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "dev": true, "license": "ISC" }, "node_modules/electron-winstaller": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "engines": { "node": ">=8.0.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "node_modules/electron-winstaller/node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" }, "engines": { "node": ">=6 <7 || >=8" } }, "node_modules/electron/node_modules/@types/node": { "version": "22.19.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "iconv-lite": "^0.6.2" } }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/enhanced-resolve": { "version": "5.20.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" } }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "dev": true, "license": "BSD-2-Clause", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/envinfo": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", "dev": true, "license": "MIT", "bin": { "envinfo": "dist/cli.js" }, "engines": { "node": ">=4" } }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true, "license": "MIT" }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-errors": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-module-lexer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-set-tostringtag": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true, "license": "MIT", "optional": true }, "node_modules/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-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "optional": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" }, "engines": { "node": ">=8.0.0" } }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, "engines": { "node": ">=4.0" } }, "node_modules/esrecurse/node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true, "license": "MIT" }, "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/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "dev": true, "license": "Apache-2.0" }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.10.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "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/extsprintf": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", "dev": true, "engines": [ "node >=0.6.0" ], "license": "MIT", "optional": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/fastify" }, { "type": "opencollective", "url": "https://opencollective.com/fastify" } ], "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, "license": "MIT", "engines": { "node": ">= 4.9.1" } }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dev": true, "license": "Apache-2.0", "dependencies": { "websocket-driver": ">=0.5.1" }, "engines": { "node": ">=0.8.0" } }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "license": "MIT", "dependencies": { "pend": "~1.2.0" } }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", "dev": true, "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } }, "node_modules/filelist/node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/filelist/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { "node": ">=10" } }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/finalhandler": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, "license": "BSD-3-Clause", "bin": { "flat": "cli.js" } }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "dev": true, "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], "license": "MIT", "engines": { "node": ">=4.0" }, "peerDependenciesMeta": { "debug": { "optional": true } } }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/foreground-child/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", "engines": { "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" }, "engines": { "node": ">=6 <7 || >=8" } }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "license": "MIT", "dependencies": { "pump": "^3.0.0" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/glob": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" }, "engines": { "node": "20 || >=22" }, "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/glob-to-regex.js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/lru-cache": { "version": "11.2.4", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, "node_modules/glob/node_modules/path-scurry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" }, "engines": { "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "dev": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" }, "engines": { "node": ">=10.0" } }, "node_modules/global-agent/node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "optional": true, "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/got": { "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" }, "engines": { "node": ">=10.19.0" }, "funding": { "url": "https://github.com/sindresorhus/got?sponsor=1" } }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true, "license": "MIT" }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-tostringtag": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/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/highlight.js": { "version": "11.11.1", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", "license": "BSD-3-Clause", "engines": { "node": ">=12.0.0" } }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, "engines": { "node": ">=10" } }, "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", "readable-stream": "^2.0.1", "wbuf": "^1.1.0" } }, "node_modules/hpack.js/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/hpack.js/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/hpack.js/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/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", "dev": true, "license": "MIT", "dependencies": { "camel-case": "^4.1.2", "clean-css": "^5.2.2", "commander": "^8.3.0", "he": "^1.2.0", "param-case": "^3.0.4", "relateurl": "^0.2.7", "terser": "^5.10.0" }, "bin": { "html-minifier-terser": "cli.js" }, "engines": { "node": ">=12" } }, "node_modules/html-minifier-terser/node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true, "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/html-webpack-plugin": { "version": "5.6.6", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.6.tgz", "integrity": "sha512-bLjW01UTrvoWTJQL5LsMRo1SypHW80FTm12OJRSnr3v6YHNhfe+1r0MYUZJMACxnCHURVnBWRwAsWs2yPU9Ezw==", "dev": true, "license": "MIT", "dependencies": { "@types/html-minifier-terser": "^6.0.0", "html-minifier-terser": "^6.0.2", "lodash": "^4.17.21", "pretty-error": "^4.0.0", "tapable": "^2.0.0" }, "engines": { "node": ">=10.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/html-webpack-plugin" }, "peerDependencies": { "@rspack/core": "0.x || 1.x", "webpack": "^5.20.0" }, "peerDependenciesMeta": { "@rspack/core": { "optional": true }, "webpack": { "optional": true } } }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { "type": "github", "url": "https://github.com/sponsors/fb55" } ], "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.0.0", "domutils": "^2.5.2", "entities": "^2.0.0" } }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", "dev": true, "license": "MIT" }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.8" } }, "node_modules/http-parser-js": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", "dev": true, "license": "MIT" }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" }, "engines": { "node": ">=8.0.0" } }, "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/http-proxy-middleware": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", "is-glob": "^4.0.1", "is-plain-obj": "^3.0.0", "micromatch": "^4.0.2" }, "engines": { "node": ">=12.0.0" }, "peerDependencies": { "@types/express": "^4.17.13" }, "peerDependenciesMeta": { "@types/express": { "optional": true } } }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "dev": true, "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" }, "engines": { "node": ">=10.19.0" } }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "4" }, "engines": { "node": ">= 14" } }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", "dev": true, "license": "MIT", "engines": { "node": ">=10.18" } }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "engines": { "node": "^8.11.2 || >=10" } }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true, "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, "peerDependencies": { "postcss": "^8.1.0" } }, "node_modules/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/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" } }, "node_modules/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/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" } }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "dev": true, "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" } }, "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.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-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-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" }, "engines": { "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-network-error": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", "integrity": "sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==", "dev": true, "license": "MIT", "engines": { "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/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/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" }, "engines": { "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, "license": "MIT" }, "node_modules/isbinaryfile": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 18.0.0" }, "funding": { "url": "https://github.com/sponsors/gjtorikian/" } }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/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-worker": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, "engines": { "node": ">= 10.13.0" } }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-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==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true, "license": "ISC", "optional": true }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" }, "engines": { "node": ">=6" } }, "node_modules/jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/katex": { "version": "0.16.25", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.25.tgz", "integrity": "sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==", "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/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/launch-editor": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.1.tgz", "integrity": "sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA==", "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.1.1", "shell-quote": "^1.8.3" } }, "node_modules/lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "dev": true, "license": "MIT" }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" } }, "node_modules/loader-runner": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" } }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, "engines": { "node": ">=8" } }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, "node_modules/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/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.3" } }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, "engines": { "node": ">=10" } }, "node_modules/make-fetch-happen": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", "dev": true, "license": "ISC", "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/make-fetch-happen/node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/markdown-it": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "node_modules/markdown-it-texmath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/markdown-it-texmath/-/markdown-it-texmath-1.0.0.tgz", "integrity": "sha512-4hhkiX8/gus+6e53PLCUmUrsa6ZWGgJW2XCW6O0ASvZUiezIK900ZicinTDtG3kAO2kon7oUA/ReWmpW2FByxg==", "license": "MIT" }, "node_modules/markdown-it/node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "escape-string-regexp": "^4.0.0" }, "engines": { "node": ">=10" } }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "license": "MIT" }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/memfs": { "version": "4.56.10", "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.10.tgz", "integrity": "sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/fs-core": "4.56.10", "@jsonjoy.com/fs-fsa": "4.56.10", "@jsonjoy.com/fs-node": "4.56.10", "@jsonjoy.com/fs-node-builtins": "4.56.10", "@jsonjoy.com/fs-node-to-fsa": "4.56.10", "@jsonjoy.com/fs-node-utils": "4.56.10", "@jsonjoy.com/fs-print": "4.56.10", "@jsonjoy.com/fs-snapshot": "4.56.10", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", "thingies": "^2.5.0", "tree-dump": "^1.0.3", "tslib": "^2.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, "license": "MIT" }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", "bin": { "mime": "cli.js" }, "engines": { "node": ">=4.0.0" } }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" } }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "dev": true, "license": "ISC" }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" }, "engines": { "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/minipass-collect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/minipass-fetch": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "engines": { "node": "^18.17.0 || >=20.5.0" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "node_modules/minipass-flush": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "dev": true, "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, "engines": { "node": ">= 8" } }, "node_modules/minipass-flush/node_modules/minipass": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "dev": true, "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, "engines": { "node": ">=8" } }, "node_modules/minipass-pipeline/node_modules/minipass": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/minipass-sized": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "dev": true, "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, "engines": { "node": ">=8" } }, "node_modules/minipass-sized/node_modules/minipass": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/minizlib": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" }, "engines": { "node": ">= 18" } }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "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==", "dev": true, "license": "MIT" }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dev": true, "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, "engines": { "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, "license": "MIT" }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "dev": true, "license": "MIT", "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "node_modules/node-abi": { "version": "4.26.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", "integrity": "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw==", "dev": true, "license": "MIT", "dependencies": { "semver": "^7.6.3" }, "engines": { "node": ">=22.12.0" } }, "node_modules/node-abi/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/node-addon-api": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", "dev": true, "license": "MIT", "optional": true }, "node_modules/node-api-version": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", "dev": true, "license": "MIT", "dependencies": { "semver": "^7.3.5" } }, "node_modules/node-api-version/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/node-forge": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/node-gyp": { "version": "11.5.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/node-gyp/node_modules/isexe": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, "node_modules/node-gyp/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/node-gyp/node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, "node_modules/nopt": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", "dev": true, "license": "ISC", "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/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-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/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, "license": "MIT", "optional": true, "engines": { "node": ">= 0.4" } }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true, "license": "MIT" }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, "engines": { "node": ">= 0.8" } }, "node_modules/on-headers": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" }, "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "dev": true, "license": "MIT", "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, "engines": { "node": ">=8" } }, "node_modules/p-map": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-retry": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", "dev": true, "license": "MIT", "dependencies": { "@types/retry": "0.12.2", "is-network-error": "^1.0.0", "retry": "^0.13.1" }, "engines": { "node": ">=16.17" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-retry/node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/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/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", "dev": true, "license": "MIT", "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/pascal-case": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "dev": true, "license": "MIT", "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true, "license": "MIT" }, "node_modules/pe-library": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", "dev": true, "license": "MIT", "engines": { "node": ">=12", "npm": ">=6" }, "funding": { "type": "github", "url": "https://github.com/sponsors/jet2jet" } }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/pkijs": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz", "integrity": "sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@noble/hashes": "1.4.0", "asn1js": "^3.0.6", "bytestreamjs": "^2.0.1", "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" }, "engines": { "node": ">=16.0.0" } }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "dev": true, "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" }, "engines": { "node": ">=10.4.0" } }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "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", "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-modules-extract-imports": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, "peerDependencies": { "postcss": "^8.1.0" } }, "node_modules/postcss-modules-local-by-default": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", "dev": true, "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "engines": { "node": "^10 || ^12 || >= 14" }, "peerDependencies": { "postcss": "^8.1.0" } }, "node_modules/postcss-modules-scope": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dev": true, "license": "ISC", "dependencies": { "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" }, "peerDependencies": { "postcss": "^8.1.0" } }, "node_modules/postcss-modules-values": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, "license": "ISC", "dependencies": { "icss-utils": "^5.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" }, "peerDependencies": { "postcss": "^8.1.0" } }, "node_modules/postcss-selector-parser": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" }, "engines": { "node": ">=4" } }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true, "license": "MIT" }, "node_modules/postject": { "version": "1.0.0-alpha.6", "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" }, "engines": { "node": ">=14.0.0" } }, "node_modules/postject/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", "optional": true, "engines": { "node": "^12.20.0 || >=14" } }, "node_modules/pretty-error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", "dev": true, "license": "MIT", "dependencies": { "lodash": "^4.17.20", "renderkid": "^3.0.0" } }, "node_modules/proc-log": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", "dev": true, "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.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/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, "license": "MIT", "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" }, "engines": { "node": ">=10" } }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" }, "engines": { "node": ">= 0.10" } }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/pvtsutils": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.8.1" } }, "node_modules/pvutils": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", "dev": true, "license": "MIT", "engines": { "node": ">=16.0.0" } }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, "node_modules/raw-body/node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, "engines": { "node": ">=0.10.0" } }, "node_modules/raw-body/node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "node_modules/read-binary-file-arch": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" }, "engines": { "node": ">= 6" } }, "node_modules/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/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, "license": "MIT", "dependencies": { "resolve": "^1.20.0" }, "engines": { "node": ">= 10.13.0" } }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, "license": "Apache-2.0" }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", "dev": true, "license": "MIT", "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", "htmlparser2": "^6.1.0", "lodash": "^4.17.21", "strip-ansi": "^6.0.1" } }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true, "license": "MIT" }, "node_modules/resedit": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", "dev": true, "license": "MIT", "dependencies": { "pe-library": "^0.4.1" }, "engines": { "node": ">=12", "npm": ">=6" }, "funding": { "type": "github", "url": "https://github.com/sponsors/jet2jet" } }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true, "license": "MIT" }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, "engines": { "node": ">=8" } }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "dev": true, "license": "MIT", "dependencies": { "lowercase-keys": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, "license": "MIT", "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" }, "engines": { "node": ">=8" } }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "node_modules/rimraf/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/rimraf/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rimraf/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/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", "dev": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" }, "engines": { "node": ">=8.0" } }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "license": "MIT" }, "node_modules/sanitize-filename": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", "dev": true, "license": "WTFPL OR ISC", "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "node_modules/sax": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" } }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 10.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" } }, "node_modules/schema-utils/node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/schema-utils/node_modules/ajv-keywords": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "node_modules/schema-utils/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", "dev": true, "license": "MIT" }, "node_modules/selfsigned": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", "dev": true, "license": "MIT", "dependencies": { "@peculiar/x509": "^1.14.2", "pkijs": "^3.3.3" }, "engines": { "node": ">=18" } }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/semver-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true, "license": "MIT", "optional": true }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/send/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/send/node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/send/node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "license": "MIT", "bin": { "mime": "cli.js" }, "engines": { "node": ">=4" } }, "node_modules/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "type-fest": "^0.13.1" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/serialize-javascript": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.3.tgz", "integrity": "sha512-h+cZ/XXarqDgCjo+YSyQU/ulDEESGGf8AMK9pPNmhNSl/FzPl6L8pMp1leca5z6NuG6tvV/auC8/43tmovowww==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=20.0.0" } }, "node_modules/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.4", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", "http-errors": "~1.6.2", "mime-types": "~2.1.17", "parseurl": "~1.3.2" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/serve-index/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/serve-index/node_modules/depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/serve-index/node_modules/http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "dev": true, "license": "MIT", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" }, "engines": { "node": ">= 0.6" } }, "node_modules/serve-index/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "dev": true, "license": "ISC" }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/serve-index/node_modules/setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", "dev": true, "license": "ISC" }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true, "license": "ISC" }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, "license": "MIT", "dependencies": { "kind-of": "^6.0.2" }, "engines": { "node": ">=8" } }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, "engines": { "node": ">=8" } }, "node_modules/shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/shell-quote": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/side-channel-list": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/side-channel-map": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/side-channel-weakmap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" }, "engines": { "node": ">=10" } }, "node_modules/simple-update-notifier/node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" }, "engines": { "node": ">=8" } }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", "dev": true, "license": "MIT", "dependencies": { "faye-websocket": "^0.11.3", "uuid": "^8.3.2", "websocket-driver": "^0.7.4" } }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "dev": true, "license": "MIT", "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { "version": "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.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.0", "handle-thing": "^2.0.0", "http-deceiver": "^1.2.7", "select-hose": "^2.0.0", "spdy-transport": "^3.0.0" }, "engines": { "node": ">=6.0.0" } }, "node_modules/spdy-transport": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.0", "detect-node": "^2.0.4", "hpack.js": "^2.1.6", "obuf": "^1.1.2", "readable-stream": "^3.0.6", "wbuf": "^1.7.3" } }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true, "license": "BSD-3-Clause", "optional": true }, "node_modules/ssri": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", "dev": true, "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/style-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", "dev": true, "license": "MIT", "engines": { "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { "webpack": "^5.27.0" } }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", "dev": true, "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" }, "engines": { "node": ">= 8.0" } }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" } }, "node_modules/tar": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" }, "engines": { "node": ">=18" } }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, "node_modules/temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" }, "engines": { "node": ">=6.0.0" } }, "node_modules/temp-file": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", "dev": true, "license": "MIT", "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "node_modules/temp-file/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { "node": ">=12" } }, "node_modules/temp-file/node_modules/jsonfile": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/temp-file/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/terser": { "version": "5.46.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" }, "engines": { "node": ">=10" } }, "node_modules/terser-webpack-plugin": { "version": "5.3.16", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { "webpack": "^5.1.0" }, "peerDependenciesMeta": { "@swc/core": { "optional": true }, "esbuild": { "optional": true }, "uglify-js": { "optional": true } } }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT" }, "node_modules/thingies": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", "dev": true, "license": "MIT", "engines": { "node": ">=10.18" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "^2" } }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true, "license": "MIT" }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", "dev": true, "license": "MIT", "dependencies": { "semver": "^5.5.0" } }, "node_modules/tiny-async-pool/node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver" } }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" }, "funding": { "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" }, "peerDependencies": { "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { "picomatch": { "optional": true } } }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "peer": true, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "dev": true, "license": "MIT", "engines": { "node": ">=14.14" } }, "node_modules/tmp-promise": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", "dev": true, "license": "MIT", "dependencies": { "tmp": "^0.2.0" } }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, "engines": { "node": ">=8.0" } }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.6" } }, "node_modules/tree-dump": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" }, "peerDependencies": { "tslib": "2" } }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "dev": true, "license": "WTFPL", "dependencies": { "utf8-byte-length": "^1.0.1" } }, "node_modules/ts-loader": { "version": "9.5.4", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", "semver": "^7.3.4", "source-map": "^0.7.4" }, "engines": { "node": ">=12.0.0" }, "peerDependencies": { "typescript": "*", "webpack": "^5.0.0" } }, "node_modules/ts-loader/node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/ts-loader/node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">= 12" } }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, "license": "0BSD", "peer": true }, "node_modules/tsyringe": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", "dev": true, "license": "MIT", "dependencies": { "tslib": "^1.9.3" }, "engines": { "node": ">= 6.0.0" } }, "node_modules/tsyringe/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true, "license": "0BSD" }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true, "license": "(MIT OR CC0-1.0)", "optional": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" }, "engines": { "node": ">= 0.6" } }, "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", "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { "node": ">=14.17" } }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, "node_modules/unique-filename": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", "dev": true, "license": "ISC", "dependencies": { "unique-slug": "^5.0.0" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/unique-slug": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" } }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" } }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "dev": true, "license": "(WTFPL OR MIT)" }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "license": "MIT" }, "node_modules/utila": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", "dev": true, "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.4.0" } }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" }, "engines": { "node": ">=0.6.0" } }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" }, "engines": { "node": ">=10.13.0" } }, "node_modules/wbuf": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "dev": true, "license": "MIT", "dependencies": { "minimalistic-assert": "^1.0.0" } }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "dev": true, "license": "MIT", "dependencies": { "defaults": "^1.0.3" } }, "node_modules/webpack": { "version": "5.105.3", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz", "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" }, "engines": { "node": ">=10.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependenciesMeta": { "webpack-cli": { "optional": true } } }, "node_modules/webpack-cli": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", "@webpack-cli/info": "^3.0.1", "@webpack-cli/serve": "^3.0.1", "colorette": "^2.0.14", "commander": "^12.1.0", "cross-spawn": "^7.0.3", "envinfo": "^7.14.0", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^3.1.1", "rechoir": "^0.8.0", "webpack-merge": "^6.0.1" }, "bin": { "webpack-cli": "bin/cli.js" }, "engines": { "node": ">=18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { "webpack": "^5.82.0" }, "peerDependenciesMeta": { "webpack-bundle-analyzer": { "optional": true }, "webpack-dev-server": { "optional": true } } }, "node_modules/webpack-cli/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/webpack-dev-middleware": { "version": "7.4.5", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", "dev": true, "license": "MIT", "dependencies": { "colorette": "^2.0.10", "memfs": "^4.43.1", "mime-types": "^3.0.1", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { "optional": true } } }, "node_modules/webpack-dev-middleware/node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/webpack-dev-middleware/node_modules/mime-types": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { "node": ">=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, "node_modules/webpack-dev-server": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.3.tgz", "integrity": "sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ==", "dev": true, "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", "@types/express": "^4.17.25", "@types/express-serve-static-core": "^4.17.21", "@types/serve-index": "^1.9.4", "@types/serve-static": "^1.15.5", "@types/sockjs": "^0.3.36", "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.2.1", "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.8.1", "connect-history-api-fallback": "^2.0.0", "express": "^4.22.1", "graceful-fs": "^4.2.6", "http-proxy-middleware": "^2.0.9", "ipaddr.js": "^2.1.0", "launch-editor": "^2.6.1", "open": "^10.0.3", "p-retry": "^6.2.0", "schema-utils": "^4.2.0", "selfsigned": "^5.5.0", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", "webpack-dev-middleware": "^7.4.2", "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { "optional": true }, "webpack-cli": { "optional": true } } }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", "dev": true, "license": "MIT", "engines": { "node": ">= 10" } }, "node_modules/webpack-merge": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "dev": true, "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", "wildcard": "^2.0.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/webpack-sources": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, "license": "MIT", "engines": { "node": ">=10.13.0" } }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" }, "engines": { "node": ">=0.8.0" } }, "node_modules/websocket-extensions": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=0.8.0" } }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" }, "engines": { "node": ">= 8" } }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true, "license": "MIT" }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "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/wsl-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", "dev": true, "license": "MIT", "dependencies": { "is-wsl": "^3.1.0" }, "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "dev": true, "license": "MIT", "engines": { "node": ">=8.0" } }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "license": "ISC" }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" }, "engines": { "node": ">=12" } }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } } } } lemonade-sdk-lemonade-dbde812/src/app/package.json000066400000000000000000000074001516551144000221720ustar00rootroot00000000000000{ "name": "lemonade", "version": "1.0.0", "description": "Lemonade Electron App", "main": "main.js", "scripts": { "start": "npm run build:renderer && electron .", "build:renderer": "webpack --mode development", "build:renderer:prod": "webpack --mode production", "watch:renderer": "webpack --watch --mode development", "dev": "npm run build:renderer && electron . --remote-debugging-port=9222", "build": "npm run build:renderer:prod && electron-builder", "build:win": "npm run build:renderer:prod && electron-builder --win", "build:mac": "npm run build:renderer:prod && electron-builder --mac", "build:linux": "npm run build:renderer:prod && electron-builder --linux", "build:appimage": "npm run build:renderer:prod && electron-builder --linux AppImage" }, "keywords": [ "electron", "lemonade", "typescript" ], "author": "", "license": "MIT", "devDependencies": { "@types/katex": "^0.16.7", "@types/node": "^20.10.5", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "css-loader": "^7.1.4", "electron": "^39.2.4", "electron-builder": "^26.8.1", "glob": ">=10.5.0", "html-webpack-plugin": "^5.6.6", "node-forge": ">=1.3.2", "style-loader": "^4.0.0", "ts-loader": "^9.5.4", "typescript": "^5.3.3", "webpack": "^5.105.3", "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.2.3" }, "dependencies": { "@types/markdown-it": "^14.1.2", "highlight.js": "^11.11.1", "katex": "^0.16.25", "markdown-it": "^14.1.0", "markdown-it-texmath": "^1.0.0", "react": "^19.2.0", "react-dom": "^19.2.0" }, "overrides": { "serialize-javascript": "^7.0.3", "lodash": "~4.17.21" }, "build": { "appId": "com.lemonade.app", "protocols": [ { "name": "Lemonade", "schemes": ["lemonade"] } ], "productName": "lemonade-app", "publish": null, "toolsets": { "appimage": "1.0.2" }, "directories": { "output": "dist-app" }, "files": [ "main.js", "preload.js", { "from": "dist/renderer", "to": "dist/renderer", "filter": [ "**/*", "!**/*.map" ] }, "!node_modules", "!src", "!**/*.ts", "!**/*.tsx", "!**/*.map" ], "extraResources": [], "asarUnpack": [], "win": { "icon": "../../docs/assets/favicon.ico", "target": [ { "target": "nsis", "arch": [ "x64" ] } ] }, "electronLanguages": [ "en-US" ], "mac": { "icon": "../../docs/assets/logo_512.png", "target": [ "dmg" ], "category": "public.app-category.business", "hardenedRuntime": true, "gatekeeperAssess": false, "entitlements": "../../src/cpp/server/entitlements.plist", "entitlementsInherit": "../../src/cpp/server/entitlements.plist" }, "linux": { "target": [ "dir" ], "category": "Utility", "icon": "../../src/app/assets/logo.svg", "desktop": { "entry": { "Name": "Lemonade App", "Comment": "Local LLMs with GPU and NPU acceleration - Desktop Application", "GenericName": "AI Model Manager", "Keywords": "AI;LLM;GPU;NPU;Machine Learning;", "Categories": "Development;Utility;" } } }, "appImage": { "artifactName": "${productName}-${version}-${arch}.${ext}", "synopsis": "Local LLM server with GPU and NPU acceleration", "category": "Development" }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, "createStartMenuShortcut": true } } } lemonade-sdk-lemonade-dbde812/src/app/preload.js000066400000000000000000000057771516551144000217070ustar00rootroot00000000000000const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('api', { writeClipboard: (text) => ipcRenderer.invoke('write-clipboard', text), // Expose any APIs you need here isWebApp: false, // Explicit flag to indicate Electron mode (vs web) platform: process.platform, minimizeWindow: () => ipcRenderer.send('minimize-window'), maximizeWindow: () => ipcRenderer.send('maximize-window'), closeWindow: () => ipcRenderer.send('close-window'), openExternal: (url) => ipcRenderer.send('open-external', url), onMaximizeChange: (callback) => { ipcRenderer.on('maximize-change', (event, isMaximized) => callback(isMaximized)); }, updateMinWidth: (width) => ipcRenderer.send('update-min-width', width), zoomIn: () => ipcRenderer.send('zoom-in'), zoomOut: () => ipcRenderer.send('zoom-out'), getSettings: () => ipcRenderer.invoke('get-app-settings'), saveSettings: (settings) => ipcRenderer.invoke('save-app-settings', settings), onSettingsUpdated: (callback) => { if (typeof callback !== 'function') { return undefined; } const channel = 'settings-updated'; const handler = (_event, payload) => { callback(payload); }; ipcRenderer.on(channel, handler); return () => { ipcRenderer.removeListener(channel, handler); }; }, getVersion: () => ipcRenderer.invoke('get-version'), discoverServerPort: () => ipcRenderer.invoke('discover-server-port'), getServerPort: () => ipcRenderer.invoke('get-server-port'), // Returns the configured server base URL, or null if using localhost discovery getServerBaseUrl: () => ipcRenderer.invoke('get-server-base-url'), getServerAPIKey: () => ipcRenderer.invoke('get-server-api-key'), onServerPortUpdated: (callback) => { if (typeof callback !== 'function') { return undefined; } const channel = 'server-port-updated'; const handler = (_event, port) => { callback(port); }; ipcRenderer.on(channel, handler); return () => { ipcRenderer.removeListener(channel, handler); }; }, onConnectionSettingsUpdated: (callback) => { if (typeof callback !== 'function') { return undefined; } const channel = 'connection-settings-updated'; const handler = (_event, baseURL, apiKey) => { callback(baseURL, apiKey); }; ipcRenderer.on(channel, handler); return () => { ipcRenderer.removeListener(channel, handler); }; }, getSystemStats: () => ipcRenderer.invoke('get-system-stats'), getSystemInfo: () => ipcRenderer.invoke('get-system-info'), getLocalMarketplaceUrl: () => ipcRenderer.invoke('get-local-marketplace-url'), signalReady: () => ipcRenderer.send('renderer-ready'), onNavigate: (callback) => { if (typeof callback !== 'function') { return undefined; } const channel = 'navigate'; const handler = (_event, data) => { callback(data); }; ipcRenderer.on(channel, handler); return () => { ipcRenderer.removeListener(channel, handler); }; }, }); lemonade-sdk-lemonade-dbde812/src/app/src/000077500000000000000000000000001516551144000204725ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/src/app/src/global.d.ts000066400000000000000000000044601516551144000225300ustar00rootroot00000000000000import type { AppSettings } from './renderer/utils/appSettings'; declare module '*.svg' { const content: string; export default content; } declare module '../../assets/*.svg' { const content: string; export default content; } declare module 'markdown-it-texmath' { import MarkdownIt from 'markdown-it'; interface TexmathOptions { engine?: any; delimiters?: 'dollars' | 'brackets' | 'gitlab' | 'kramdown'; katexOptions?: any; } function texmath(md: MarkdownIt, options?: TexmathOptions): void; export = texmath; } declare global { interface Window { api: { writeClipboard?: (text: string) => Promise; isWebApp?: boolean; // Explicit flag to indicate web mode (vs Electron) platform: string; minimizeWindow: () => void; maximizeWindow: () => void; closeWindow: () => void; openExternal: (url: string) => void; onMaximizeChange: (callback: (isMaximized: boolean) => void) => void; updateMinWidth: (width: number) => void; zoomIn: () => void; zoomOut: () => void; getSettings?: () => Promise; saveSettings?: (settings: AppSettings) => Promise; onSettingsUpdated?: (callback: (settings: AppSettings) => void) => void | (() => void); getVersion?: () => Promise; discoverServerPort?: () => Promise; getServerPort?: () => Promise; // Returns the configured server base URL or null if using localhost discovery getServerBaseUrl?: () => Promise; getServerAPIKey?: () => Promise; onServerPortUpdated?: (callback: (port: number) => void) => void | (() => void); onConnectionSettingsUpdated?: (callback: (baseURL: string, apiKey: string) => void) => void | (() => void); getSystemStats?: () => Promise<{ cpu_percent: number | null; memory_gb: number; gpu_percent: number | null; vram_gb: number | null; npu_percent: number | null }>; getSystemInfo?: () => Promise<{ system: string; os: string; cpu: string; gpus: string[]; gtt_gb?: string; vram_gb?: string }>; getLocalMarketplaceUrl?: () => Promise; signalReady?: () => void; onNavigate?: (callback: (data: { view?: string; model?: string }) => void) => void | (() => void); }; } } export {}; lemonade-sdk-lemonade-dbde812/src/app/src/renderer/000077500000000000000000000000001516551144000223005ustar00rootroot00000000000000lemonade-sdk-lemonade-dbde812/src/app/src/renderer/AboutModal.tsx000066400000000000000000000150201516551144000250650ustar00rootroot00000000000000import React, { useEffect, useRef, useState } from 'react'; import { fetchSystemInfoData } from './utils/systemData'; interface AboutModalProps { isOpen: boolean; onClose: () => void; } interface SystemInfo { system: string; os: string; cpu: string; gpus: string[]; gtt_gb?: string; vram_gb?: string; } const AboutModal: React.FC = ({ isOpen, onClose }) => { const [version, setVersion] = useState('Loading...'); const [systemInfo, setSystemInfo] = useState(null); const [isLoadingInfo, setIsLoadingInfo] = useState(false); const cardRef = useRef(null); useEffect(() => { if (isOpen && window.api?.getVersion) { setVersion('Loading...'); setIsLoadingInfo(true); // Retry logic to handle backend startup delay const fetchVersionWithRetry = async (retries = 3, delay = 1000) => { for (let i = 0; i < retries; i++) { const v = await window.api.getVersion!(); if (v !== 'Unknown') { setVersion(v); return; } if (i < retries - 1) { await new Promise(resolve => setTimeout(resolve, delay)); } } setVersion('Unknown (Backend not running)'); }; const fetchSystemInfo = async () => { try { if (window.api?.getSystemInfo) { const info = await window.api.getSystemInfo(); console.log('SystemInfo received in AboutModal:', info); setSystemInfo(info); return; } const { info } = await fetchSystemInfoData(); if (!info) { return; } const gpus: string[] = []; let maxGttGb = 0; let maxVramGb = 0; const considerAmdGpu = (gpu?: { name?: string; virtual_mem_gb?: number; vram_gb?: number }) => { if (!gpu) return; if (gpu.name) gpus.push(gpu.name); if (typeof gpu.virtual_mem_gb === 'number' && isFinite(gpu.virtual_mem_gb)) { maxGttGb = Math.max(maxGttGb, gpu.virtual_mem_gb); } if (typeof gpu.vram_gb === 'number' && isFinite(gpu.vram_gb)) { maxVramGb = Math.max(maxVramGb, gpu.vram_gb); } }; considerAmdGpu(info.devices?.amd_igpu); info.devices?.amd_dgpu?.forEach(considerAmdGpu); info.devices?.nvidia_dgpu?.forEach((gpu) => { if (gpu?.name) gpus.push(gpu.name); }); const normalized: SystemInfo = { system: 'Unknown', os: info.os_version || 'Unknown', cpu: info.processor || 'Unknown', gpus, gtt_gb: maxGttGb > 0 ? `${maxGttGb} GB` : 'Unknown', vram_gb: maxVramGb > 0 ? `${maxVramGb} GB` : 'Unknown', }; setSystemInfo(normalized); } catch (error) { console.error('Failed to fetch system info:', error); } finally { setIsLoadingInfo(false); } }; fetchVersionWithRetry(); fetchSystemInfo(); } }, [isOpen]); useEffect(() => { if (!isOpen) return; const handleClickOutside = (event: MouseEvent) => { if (cardRef.current && !cardRef.current.contains(event.target as Node)) { onClose(); } }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { onClose(); } }; document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleKeyDown); }; }, [isOpen, onClose]); if (!isOpen) return null; return (

Lemonade

Local AI control center

Version {version}
{!isLoadingInfo && systemInfo && ( <> {systemInfo.system && systemInfo.system !== 'Unknown' && (
System {systemInfo.system}
)} {systemInfo.os && systemInfo.os !== 'Unknown' && (
OS {systemInfo.os}
)} {systemInfo.cpu && systemInfo.cpu !== 'Unknown' && (
CPU {systemInfo.cpu}
)} {systemInfo.gpus.length > 0 && (
GPU{systemInfo.gpus.length > 1 ? 's' : ''} {systemInfo.gpus.join(', ')}
)} {systemInfo.gtt_gb && systemInfo.gtt_gb !== 'Unknown' && (
Shared GPU memory {systemInfo.gtt_gb}
)} {systemInfo.vram_gb && systemInfo.vram_gb !== 'Unknown' && (
Dedicated GPU memory {systemInfo.vram_gb}
)} )}
); }; export default AboutModal; lemonade-sdk-lemonade-dbde812/src/app/src/renderer/AddModelPanel.tsx000066400000000000000000000220441516551144000254730ustar00rootroot00000000000000import React, { useState, useEffect } from 'react'; import { useSystem } from './hooks/useSystem'; export interface AddModelInitialValues { name: string; checkpoint: string; recipe: string; mmprojOptions?: string[]; vision?: boolean; reranking?: boolean; embedding?: boolean; } export interface ModelInstallData { name: string; checkpoint: string; recipe: string; mmproj?: string; reasoning?: boolean; vision?: boolean; embedding?: boolean; reranking?: boolean; } interface AddModelPanelProps { onClose: () => void; onInstall: (data: ModelInstallData) => void; initialValues?: AddModelInitialValues; } const RECIPE_LABELS: Record = { 'llamacpp': 'Llama.cpp GPU', 'flm': 'FastFlowLM NPU', 'ryzenai-llm': 'Ryzen AI LLM', }; const createEmptyForm = (initial?: AddModelInitialValues) => ({ name: initial?.name ?? '', checkpoint: initial?.checkpoint ?? '', recipe: initial?.recipe ?? 'llamacpp', mmproj: '', reasoning: false, vision: initial?.vision ?? false, embedding: initial?.embedding ?? false, reranking: initial?.reranking ?? false, }); const AddModelPanel: React.FC = ({ onClose, onInstall, initialValues }) => { const { supportedRecipes } = useSystem(); const [form, setForm] = useState(() => createEmptyForm(initialValues)); const [error, setError] = useState(null); const mmprojOptions = initialValues?.mmprojOptions ?? []; const getMmprojLabel = (filename: string): string => filename.replace(/^mmproj-/i, '').replace(/^model-/i, '').replace(/\.gguf$/i, ''); useEffect(() => { const newForm = createEmptyForm(initialValues); if (initialValues?.mmprojOptions && initialValues.mmprojOptions.length > 0) { newForm.mmproj = initialValues.mmprojOptions[0]; } setForm(newForm); setError(null); }, [initialValues]); const handleChange = (field: string, value: string | boolean) => { setForm(prev => ({ ...prev, [field]: value })); setError(null); }; const handleInstall = () => { const name = form.name.trim(); const checkpoint = form.checkpoint.trim(); const recipe = form.recipe.trim(); if (!name) { setError('Model name is required.'); return; } if (!checkpoint) { setError('Checkpoint is required.'); return; } if (!recipe) { setError('Recipe is required.'); return; } if (checkpoint.toLowerCase().includes('gguf') && !checkpoint.includes(':')) { setError('GGUF checkpoints must include a variant using the CHECKPOINT:VARIANT syntax.'); return; } onInstall({ name, checkpoint, recipe, mmproj: form.mmproj.trim() || undefined, reasoning: form.reasoning, vision: form.vision, embedding: form.embedding, reranking: form.reranking, }); }; const filteredSupportedRecipes = Object.keys(supportedRecipes).filter(r => r in RECIPE_LABELS); const recipeOptions = filteredSupportedRecipes.length > 0 ? filteredSupportedRecipes : Object.keys(RECIPE_LABELS); const mmprojOptionElements = mmprojOptions.map((f: string) => { const label = getMmprojLabel(f); return React.createElement('option', { key: f, value: f }, label); }); const showMmproj = mmprojOptions.length > 0 || !initialValues; const mmprojField: React.ReactNode = showMmproj ? React.createElement( 'div', { className: 'form-subsection' }, React.createElement( 'label', { className: 'form-label-secondary', title: 'Multimodal projection file for vision models' }, 'mmproj file (Optional)' ), mmprojOptions.length > 0 ? React.createElement( 'select', { className: 'form-input form-select', value: form.mmproj, onChange: (e: React.ChangeEvent) => handleChange('mmproj', e.target.value), }, ...mmprojOptionElements ) : React.createElement('input', { type: 'text', className: 'form-input', placeholder: 'mmproj-F16.gguf', value: form.mmproj, onChange: (e: React.ChangeEvent) => handleChange('mmproj', e.target.value), }) ) : null; return ( <>

Add a Model

user. handleChange('name', e.target.value)} />
handleChange('checkpoint', e.target.value)} />
{mmprojField}
{error &&
{error}
}
); }; export default AddModelPanel; lemonade-sdk-lemonade-dbde812/src/app/src/renderer/App.tsx000066400000000000000000000331551516551144000235670ustar00rootroot00000000000000import React, { useState, useEffect, useRef, useCallback } from 'react'; import { ChevronLeft } from './components/Icons'; import TitleBar from './TitleBar'; import ChatWindow from './ChatWindow'; import ModelManager, { LeftPanelView } from './ModelManager'; import LogsWindow from './LogsWindow'; import ResizableDivider from './ResizableDivider'; import DownloadManager from './DownloadManager'; import StatusBar from './StatusBar'; import { ModelsProvider } from './hooks/useModels'; import { SystemProvider } from './hooks/useSystem'; import { DEFAULT_LAYOUT_SETTINGS } from './utils/appSettings'; import '../../styles.css'; const LAYOUT_CONSTANTS = { modelManagerMinWidth: 200, experienceRailWidth: 40, mainContentMinWidth: 300, chatMinWidth: 250, dividerWidth: 4, absoluteMinWidth: 400, }; // Inner component that can use SystemProvider context const AppContent: React.FC = () => { const [isChatVisible, setIsChatVisible] = useState(DEFAULT_LAYOUT_SETTINGS.isChatVisible); const [isModelManagerVisible, setIsModelManagerVisible] = useState(DEFAULT_LAYOUT_SETTINGS.isModelManagerVisible); const [leftPanelView, setLeftPanelView] = useState('models'); const [externalContentUrl, setExternalContentUrl] = useState(null); const [isLogsVisible, setIsLogsVisible] = useState(DEFAULT_LAYOUT_SETTINGS.isLogsVisible); const [isDownloadManagerVisible, setIsDownloadManagerVisible] = useState(false); const [modelManagerWidth, setModelManagerWidth] = useState(DEFAULT_LAYOUT_SETTINGS.modelManagerWidth); const [chatWidth, setChatWidth] = useState(DEFAULT_LAYOUT_SETTINGS.chatWidth); const [logsHeight, setLogsHeight] = useState(DEFAULT_LAYOUT_SETTINGS.logsHeight); const [layoutLoaded, setLayoutLoaded] = useState(false); const isDraggingRef = useRef<'left' | 'right' | 'bottom' | null>(null); const startXRef = useRef(0); const startYRef = useRef(0); const startWidthRef = useRef(0); const startHeightRef = useRef(0); // Load saved layout settings on mount useEffect(() => { const loadLayoutSettings = async () => { try { if (window?.api?.getSettings) { const settings = await window.api.getSettings(); if (settings.layout) { setIsChatVisible(settings.layout.isChatVisible ?? DEFAULT_LAYOUT_SETTINGS.isChatVisible); setIsModelManagerVisible(settings.layout.isModelManagerVisible ?? DEFAULT_LAYOUT_SETTINGS.isModelManagerVisible); const savedView = settings.layout.leftPanelView; if (savedView === 'models' || savedView === 'marketplace' || savedView === 'backends' || savedView === 'settings') { setLeftPanelView(savedView); } setIsLogsVisible(settings.layout.isLogsVisible ?? DEFAULT_LAYOUT_SETTINGS.isLogsVisible); setModelManagerWidth(settings.layout.modelManagerWidth ?? DEFAULT_LAYOUT_SETTINGS.modelManagerWidth); setChatWidth(settings.layout.chatWidth ?? DEFAULT_LAYOUT_SETTINGS.chatWidth); setLogsHeight(settings.layout.logsHeight ?? DEFAULT_LAYOUT_SETTINGS.logsHeight); } } } catch (error) { console.error('Failed to load layout settings:', error); } finally { // Override with URL parameters if present const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('view') === 'logs') { setIsLogsVisible(true); } setLayoutLoaded(true); } }; loadLayoutSettings(); }, []); // Save layout settings when they change (debounced) const saveLayoutSettings = useCallback(async () => { if (!layoutLoaded) return; try { if (window?.api?.getSettings && window?.api?.saveSettings) { // Get current settings and merge layout changes const currentSettings = await window.api.getSettings(); await window.api.saveSettings({ ...currentSettings, layout: { isChatVisible, isModelManagerVisible, leftPanelView, isLogsVisible, modelManagerWidth, chatWidth, logsHeight, }, }); } } catch (error) { console.error('Failed to save layout settings:', error); } }, [layoutLoaded, isChatVisible, isModelManagerVisible, leftPanelView, isLogsVisible, modelManagerWidth, chatWidth, logsHeight]); // Debounced save effect useEffect(() => { if (!layoutLoaded) return; const timeoutId = setTimeout(saveLayoutSettings, 300); return () => clearTimeout(timeoutId); }, [saveLayoutSettings, layoutLoaded]); // Listen for download start events to automatically open download manager // and download completion events from chat to close it useEffect(() => { const handleDownloadStart = () => { setIsDownloadManagerVisible(true); }; // When a chat-initiated download completes, minimize the download manager const handleChatDownloadComplete = () => { setIsDownloadManagerVisible(false); }; window.addEventListener('download:started' as any, handleDownloadStart); window.addEventListener('download:chatComplete' as any, handleChatDownloadComplete); const handleOpenExternalContent = (e: any) => { if (e.detail?.url) { setExternalContentUrl(e.detail.url); setIsChatVisible(true); setIsDownloadManagerVisible(false); } }; window.addEventListener('open-external-content' as any, handleOpenExternalContent); return () => { window.removeEventListener('download:started' as any, handleDownloadStart); window.removeEventListener('download:chatComplete' as any, handleChatDownloadComplete); window.removeEventListener('open-external-content' as any, handleOpenExternalContent); }; }, []); useEffect(() => { const handleExperienceModeChanged = (event: Event) => { const customEvent = event as CustomEvent<{ active?: boolean }>; if (customEvent.detail?.active) { setIsModelManagerVisible(false); } }; window.addEventListener('experienceModeChanged' as any, handleExperienceModeChanged); return () => { window.removeEventListener('experienceModeChanged' as any, handleExperienceModeChanged); }; }, []); // Handle lemonade:// protocol navigation from main process useEffect(() => { if (!window?.api?.onNavigate) return; const unsubscribe = window.api.onNavigate((data: { view?: string; model?: string }) => { if (data.view === 'logs') { setIsLogsVisible(true); } }); // Tell main process that IPC listeners are active โ€” safe to deliver pending nav window?.api?.signalReady?.(); return unsubscribe; }, []); useEffect(() => { const hasMainColumn = isLogsVisible; let computedMinWidth = LAYOUT_CONSTANTS.experienceRailWidth; // Rail always visible if (isModelManagerVisible) { computedMinWidth += LAYOUT_CONSTANTS.modelManagerMinWidth; } if (hasMainColumn) { computedMinWidth += LAYOUT_CONSTANTS.mainContentMinWidth; } if (isChatVisible) { computedMinWidth += LAYOUT_CONSTANTS.chatMinWidth; } let dividerCount = 0; if (isModelManagerVisible && (hasMainColumn || isChatVisible)) { dividerCount += 1; } if (hasMainColumn && isChatVisible) { dividerCount += 1; } computedMinWidth += dividerCount * LAYOUT_CONSTANTS.dividerWidth; const targetWidth = Math.max(computedMinWidth, LAYOUT_CONSTANTS.absoluteMinWidth); if (window?.api?.updateMinWidth) { window.api.updateMinWidth(targetWidth); } }, [isModelManagerVisible, isLogsVisible, isChatVisible]); useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!isDraggingRef.current) return; if (isDraggingRef.current === 'left') { // Dragging left divider (model manager) const delta = e.clientX - startXRef.current; const newWidth = Math.max(200, Math.min(500, startWidthRef.current + delta)); setModelManagerWidth(newWidth); } else if (isDraggingRef.current === 'right') { // Dragging right divider (chat window) // For right-side panel: drag left = increase width, drag right = decrease width const delta = startXRef.current - e.clientX; // Base chat width from drag let proposedWidth = startWidthRef.current + delta; // Compute maximum allowed width so the center panel (welcome screen/logs) // can respect its minimum width and the layout doesn't overflow horizontally. const appLayout = document.querySelector('.app-layout') as HTMLElement | null; const appWidth = appLayout?.clientWidth || window.innerWidth; const leftPanelWidth = isModelManagerVisible ? modelManagerWidth + LAYOUT_CONSTANTS.experienceRailWidth : LAYOUT_CONSTANTS.experienceRailWidth; const hasCenterColumn = isLogsVisible; const minCenterWidth = hasCenterColumn ? 300 : 0; // keep in sync with CSS min-width // Account for vertical dividers (each 4px wide) const dividerCount = ((isModelManagerVisible && (hasCenterColumn || isChatVisible)) ? 1 : 0) + ((hasCenterColumn && isChatVisible) ? 1 : 0); const dividerSpace = dividerCount * 4; const maxWidthFromLayout = appWidth - leftPanelWidth - minCenterWidth - dividerSpace; const minChatWidth = 250; const maxChatWidth = Math.max( minChatWidth, Math.min(800, isFinite(maxWidthFromLayout) ? maxWidthFromLayout : 800) ); const newWidth = Math.max(minChatWidth, Math.min(maxChatWidth, proposedWidth)); setChatWidth(newWidth); } else if (isDraggingRef.current === 'bottom') { // Dragging bottom divider (logs window) const delta = startYRef.current - e.clientY; const newHeight = Math.max(100, Math.min(400, startHeightRef.current + delta)); setLogsHeight(newHeight); } }; const handleMouseUp = () => { isDraggingRef.current = null; document.body.style.cursor = ''; document.body.style.userSelect = ''; }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }, [ chatWidth, isChatVisible, isModelManagerVisible, isLogsVisible, modelManagerWidth, logsHeight, ]); const handleLeftDividerMouseDown = (e: React.MouseEvent) => { isDraggingRef.current = 'left'; startXRef.current = e.clientX; startWidthRef.current = modelManagerWidth; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }; const handleRightDividerMouseDown = (e: React.MouseEvent) => { isDraggingRef.current = 'right'; startXRef.current = e.clientX; startWidthRef.current = chatWidth; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }; const handleCloseDownloadManager = useCallback(() => { setIsDownloadManagerVisible(false); }, []); return ( setIsChatVisible(!isChatVisible)} isModelManagerVisible={isModelManagerVisible} onToggleModelManager={() => setIsModelManagerVisible(!isModelManagerVisible)} isLogsVisible={isLogsVisible} onToggleLogs={() => setIsLogsVisible(!isLogsVisible)} isDownloadManagerVisible={isDownloadManagerVisible} onToggleDownloadManager={() => setIsDownloadManagerVisible(!isDownloadManagerVisible)} />
{isModelManagerVisible && (isLogsVisible || isChatVisible) && ( )} {isLogsVisible && (
)} {isChatVisible && ( <> {isLogsVisible && ( )} {externalContentUrl ? (