diff --git a/.github/workflows/docker-celery-smoke-test.yml b/.github/workflows/docker-celery-smoke-test.yml new file mode 100644 index 000000000..e4aa39b86 --- /dev/null +++ b/.github/workflows/docker-celery-smoke-test.yml @@ -0,0 +1,90 @@ +name: Docker Celery Smoke Test + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + smoke: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and start stack + run: | + docker compose build + docker compose up -d + + # Wait for server to be ready with retries + for i in {1..30}; do + if curl -sf http://127.0.0.1:8000/api/health >/dev/null 2>&1; then + echo "Server is ready" + break + fi + echo "Waiting for server... attempt $i/30" + sleep 1 + done + if ! curl -sf http://127.0.0.1:8000/api/health >/dev/null 2>&1; then + echo "Server did not become ready in time" + docker compose logs web worker postgres redis || true + exit 1 + fi + + - name: Verify service health + run: | + docker compose ps + # Ensure key services are up before API checks + docker compose ps postgres | grep -E "Up|running" + docker compose ps redis | grep -E "Up|running" + docker compose ps web | grep -E "Up|running" + docker compose ps worker | grep -E "Up|running" + + - name: Check /api/health + run: curl --fail http://127.0.0.1:8000/api/health + + - name: Check /docs + run: curl --fail http://127.0.0.1:8000/docs + + - name: Task create, interrupt, and poll terminal state + run: | + set -e + curl -s -o /dev/null -c cookies.txt -X POST http://127.0.0.1:8000/api/auth/signup -H "Content-Type: application/json" -d "{\"email\":\"user$RANDOM@test.com\", \"password\":\"password\", \"name\":\"test\"}" + TASK_JSON=$(curl -b cookies.txt -s -X POST http://127.0.0.1:8000/api/tasks -H "Content-Type: application/json" -d '{"prompt":"ping"}') + echo "Created: $TASK_JSON" + TASK_ID=$(echo "$TASK_JSON" | python -c "import sys, json; print(json.load(sys.stdin)['task_id'])") + if [ -z "$TASK_ID" ]; then echo "No task id"; exit 1; fi + + # Interrupt quickly so this smoke test does not depend on LLM latency. + curl -b cookies.txt -s -X POST "http://127.0.0.1:8000/api/tasks/$TASK_ID/interrupt" || true + + for i in {1..60}; do + STATUS_JSON=$(curl -b cookies.txt -s http://127.0.0.1:8000/api/tasks/$TASK_ID) + echo "Status: $STATUS_JSON" + STATUS=$(echo "$STATUS_JSON" | python -c "import sys, json; print(json.load(sys.stdin).get('status'))") + if [ "$STATUS" = "COMPLETED" ] || [ "$STATUS" = "FAILED" ] || [ "$STATUS" = "INTERRUPTED" ]; then + if [ "$STATUS" = "INTERRUPTED" ]; then + echo "Task interrupted as expected" + fi + exit 0 + fi + sleep 2 + done + echo "Task did not complete in time" + exit 1 + + - name: Dump logs on failure + if: failure() + run: | + docker compose ps + docker compose logs web worker postgres redis + + - name: Tear down + if: always() + run: docker compose down -v diff --git a/.github/workflows/docker-smoke-test.yml b/.github/workflows/docker-smoke-test.yml new file mode 100644 index 000000000..9ac4fb854 --- /dev/null +++ b/.github/workflows/docker-smoke-test.yml @@ -0,0 +1,141 @@ +name: Docker Smoke Test + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + smoke: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: openmanus + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + redis: + image: redis:latest + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build image + run: docker build -t openmanus:latest . + + - name: Run container + run: | + docker run -d --network host --name openmanus \ + -e DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/openmanus \ + -e CELERY_BROKER_URL=redis://localhost:6379/0 \ + -e CELERY_RESULT_BACKEND=redis://localhost:6379/0 \ + openmanus:latest + # start websocket server inside container on port 8001 + docker exec openmanus sh -c "uvicorn server.ws:app --host 0.0.0.0 --port 8001 --workers 1 >/tmp/ws.log 2>&1 & echo \$! >/tmp/ws.pid" + + # Wait for server to be ready with retries + for i in {1..30}; do + if curl -sf http://127.0.0.1:8000/api/health >/dev/null 2>&1; then + echo "Server is ready" + break + fi + echo "Waiting for server... attempt $i/30" + sleep 1 + done + if ! curl -sf http://127.0.0.1:8000/api/health >/dev/null 2>&1; then + echo "Server did not become ready in time" + docker logs openmanus || true + exit 1 + fi + + # Wait for websocket server readiness + for i in {1..20}; do + if curl -sf http://127.0.0.1:8001/health >/dev/null 2>&1; then + echo "WebSocket server is ready" + break + fi + echo "Waiting for websocket server... attempt $i/20" + sleep 1 + done + + - name: Check /api/health + run: curl --fail http://127.0.0.1:8000/api/health + + - name: Check /docs + run: curl --fail http://127.0.0.1:8000/docs + + - name: Install tooling for task test + run: | + sudo apt-get update && sudo apt-get install -y jq + python -m pip install --upgrade pip + pip install websockets + + - name: Simulate task lifecycle with interrupt and event stream + run: | + set -e + curl -s -o /dev/null -c cookies.txt -X POST http://127.0.0.1:8000/api/auth/signup -H "Content-Type: application/json" -d "{\"email\":\"user$RANDOM@test.com\", \"password\":\"password\", \"name\":\"test\"}" + TASK_JSON=$(curl -b cookies.txt -s -X POST http://127.0.0.1:8000/api/tasks) + echo "Task created: $TASK_JSON" + TASK_ID=$(echo "$TASK_JSON" | jq -r '.task_id') + if [ -z "$TASK_ID" ] || [ "$TASK_ID" = "null" ]; then + echo "Failed to parse task id"; exit 1; fi + export TASK_ID + + python - <<'PY' + import asyncio, json, websockets, os, sys + task_id = os.environ["TASK_ID"] + uri = f"ws://127.0.0.1:8001/tasks/{task_id}/stream" + events = [] + async def main(): + try: + async with websockets.connect(uri) as ws: + # send interrupt quickly to avoid long-running agent + await ws.send(json.dumps({"command": "interrupt"})) + try: + for _ in range(10): + msg = await asyncio.wait_for(ws.recv(), timeout=10) + events.append(msg) + if len(events) >= 2: + break + except asyncio.TimeoutError: + pass + except Exception as e: + print("WebSocket error:", e, file=sys.stderr) + sys.exit(1) + asyncio.run(main()) + if not events: + print("No events received"); sys.exit(1) + print("Received events:", events) + PY + + - name: Dump container logs on failure + if: failure() + run: | + docker logs openmanus || true + docker exec openmanus cat /tmp/ws.log || true + docker exec openmanus cat /tmp/server.log || true + + - name: Stop container + if: always() + run: | + docker stop openmanus || true + docker rm openmanus || true diff --git a/.github/workflows/web-smoke-test.yml b/.github/workflows/web-smoke-test.yml new file mode 100644 index 000000000..b3862fffb --- /dev/null +++ b/.github/workflows/web-smoke-test.yml @@ -0,0 +1,107 @@ +name: Web Smoke Test + +on: + push: + pull_request: + +jobs: + smoke: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: openmanus + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Wait for PostgreSQL + run: | + python - <<'PY' + import time + import psycopg2 + + for i in range(30): + try: + conn = psycopg2.connect( + host="localhost", + port=5432, + user="postgres", + password="postgres", + dbname="openmanus", + ) + conn.close() + print("PostgreSQL is ready") + raise SystemExit(0) + except Exception as exc: + print(f"Waiting for PostgreSQL... {i+1}/30 ({exc})") + time.sleep(1) + print("PostgreSQL not ready in time") + raise SystemExit(1) + PY + + - name: Start FastAPI server + env: + DATABASE_URL: postgresql+psycopg2://postgres:postgres@localhost:5432/openmanus + run: | + uvicorn server.api:app --host 0.0.0.0 --port 8000 --workers 1 > /tmp/server.log 2>&1 & + echo $! > server.pid + + # Wait for server to be ready with retries + for i in {1..30}; do + if curl -sf http://127.0.0.1:8000/api/health >/dev/null 2>&1; then + echo "Server is ready" + break + fi + echo "Waiting for server... attempt $i/30" + sleep 1 + done + + - name: Check /api/health + run: | + for i in {1..10}; do + if curl -sf http://127.0.0.1:8000/api/health > /tmp/health.json; then + cat /tmp/health.json + exit 0 + fi + sleep 2 + done + echo "Health endpoint not responding" + cat /tmp/server.log || true + exit 1 + + - name: Check /docs + run: | + curl -sf http://127.0.0.1:8000/docs > /tmp/docs.html + grep -i "swagger" /tmp/docs.html + + - name: Stop server + if: always() + run: | + if [ -f server.pid ]; then kill $(cat server.pid) || true; fi + + - name: Dump server logs on failure + if: failure() + run: | + cat /tmp/server.log || true diff --git a/.gitignore b/.gitignore index 41dbbf2d8..818e6fcf0 100644 --- a/.gitignore +++ b/.gitignore @@ -134,7 +134,7 @@ __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid - +OpenManus.code-workspace # SageMath parsed files *.sage.py diff --git a/.openhands/skills/code-review.md b/.openhands/skills/code-review.md new file mode 100644 index 000000000..d4cfa3d68 --- /dev/null +++ b/.openhands/skills/code-review.md @@ -0,0 +1,18 @@ +--- +name: code-review +type: knowledge +version: 1.0 +agent: Manus +triggers: + - review + - bug + - regression + - security + - test +--- +Code review workflow: +1. Prioritize correctness, regressions, missing tests, and security issues. +2. Ground findings in exact files, functions, or observable behavior. +3. Verify suspicious behavior by reading the caller and callee, not just one file. +4. Keep style-only notes secondary unless they block maintainability. +5. When implementing fixes, run focused tests and inspect the resulting diff. diff --git a/.openhands/skills/docker.md b/.openhands/skills/docker.md new file mode 100644 index 000000000..25259dcaa --- /dev/null +++ b/.openhands/skills/docker.md @@ -0,0 +1,18 @@ +--- +name: docker +type: knowledge +version: 1.0 +agent: Manus +triggers: + - docker + - container + - compose + - port + - server +--- +Docker workflow: +1. Inspect `docker compose ps`, container logs, and exposed ports before guessing. +2. Keep long-running dev servers in the background and write logs to a file in the conversation workspace. +3. After starting a server, verify it is listening with `ss -tlnp` or `curl`. +4. Report the reachable URL and leave enough runtime evidence for the UI process panel. +5. Do not stop OpenManus system containers unless explicitly asked. diff --git a/.openhands/skills/frontend.md b/.openhands/skills/frontend.md new file mode 100644 index 000000000..4819ef9d0 --- /dev/null +++ b/.openhands/skills/frontend.md @@ -0,0 +1,19 @@ +--- +name: frontend-ui +type: knowledge +version: 1.0 +agent: Manus +triggers: + - ui + - frontend + - react + - page + - component + - css +--- +Frontend workflow: +1. Inspect existing components, routes, services, and styling utilities before editing. +2. Prefer stable, scrollable layouts with clear empty/loading/error states. +3. Use existing icons and controls; avoid text overflow inside buttons, badges, and panels. +4. Build or type-check after changes, and use browser verification when a visual change is risky. +5. Keep the primary user flow usable before adding decorative polish. diff --git a/.openhands/skills/pdflatex.md b/.openhands/skills/pdflatex.md new file mode 100644 index 000000000..23e70c6b2 --- /dev/null +++ b/.openhands/skills/pdflatex.md @@ -0,0 +1,18 @@ +--- +name: pdflatex +type: knowledge +version: 1.0 +agent: Manus +triggers: + - latex + - tex + - pdf + - thesis + - compile +--- +LaTeX workflow: +1. Keep `.tex`, generated assets, `.log`, and final `.pdf` in the conversation workspace. +2. Compile with `latexmk -pdf -interaction=nonstopmode` when available, otherwise use repeated `pdflatex`. +3. Do not claim success until a non-empty PDF exists. +4. If no PDF exists, inspect the `.log`, fix the first real error, and compile again. +5. Summarize the output filename and any remaining warnings. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 84be37205..d9f765f23 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: black - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/Dockerfile b/Dockerfile index 9f7a19081..f2d3d6359 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,54 @@ +# syntax=docker/dockerfile:1.6 + FROM python:3.12-slim -WORKDIR /app/OpenManus +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +WORKDIR /app + +# Install dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + python3-dev \ + libpq-dev \ + libnss3 \ + libnspr4 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxrandr2 \ + libgbm1 \ + libasound2 \ + libpango-1.0-0 \ + libcairo2 \ + fonts-liberation \ + libxfixes3 \ + docker.io \ + texlive-base \ + texlive-latex-recommended \ + texlive-latex-extra \ + texlive-fonts-recommended \ + texlive-fonts-extra \ + texlive-science \ + texlive-bibtex-extra \ + latexmk \ + && rm -rf /var/lib/apt/lists/* -RUN apt-get update && apt-get install -y --no-install-recommends git curl \ - && rm -rf /var/lib/apt/lists/* \ - && (command -v uv >/dev/null 2>&1 || pip install --no-cache-dir uv) +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt \ + && playwright install chromium \ + && python -c "import cloakbrowser; cloakbrowser.ensure_binary()" \ + && echo "CloakBrowser binary pre-downloaded" COPY . . -RUN uv pip install --system -r requirements.txt +EXPOSE 8000 -CMD ["bash"] +CMD ["uvicorn", "server.api:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..ff4f71b86 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +.PHONY: web-smoke build + +web-smoke: + ./scripts/local_web_smoke.sh + +build: + docker system prune -f && docker builder prune -f + docker compose up -d --build diff --git a/README.md b/README.md index d4a84374f..6f382bcef 100644 --- a/README.md +++ b/README.md @@ -1,195 +1,186 @@

- + OpenManus

-English | [中文](README_zh.md) | [한국어](README_ko.md) | [日本語](README_ja.md) +English only. -[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers) -  -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)   -[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z) -[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo) -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407) +# OpenManus -# 👋 OpenManus +**A conversation-first agent runtime with persistent sandbox computers, live observability, and practical team workflows.** -Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫! +This fork focuses on real daily usage: long-running conversations, isolated workspaces, runtime controls, and a UI that makes the agent’s work visible and manageable. -Our team members [@Xinbin Liang](https://github.com/mannaandpoem) and [@Jinyu Xiang](https://github.com/XiangJinyu) (core authors), along with [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), and [@Sirui Hong](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building! +## Why This Repo -It's a simple implementation, so we welcome any suggestions, contributions, and feedback! +OpenManus here is built for people who want an agent they can actually run, inspect, and improve: -Enjoy your own agent with OpenManus! +- Persistent conversations with replayable history +- Per-conversation sandbox + workspace isolation +- Live tool traces, terminal output, browser screenshots, and token counts +- Runtime controls for processes, ports, and spawned containers +- Optional RL policy scaffold compatible with OpenManus-RL workflows -We're also excited to introduce [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL), an open-source project dedicated to reinforcement learning (RL)- based (such as GRPO) tuning methods for LLM agents, developed collaboratively by researchers from UIUC and OpenManus. +## What’s Implemented -## Project Demo +- Auth + admin bootstrap (first signup becomes admin) +- Session-based web auth and admin settings UI/API +- Conversation model with grouped tasks and follow-up continuity +- Per-conversation files under `workspace/conversations//` +- Reused sandbox computer per conversation +- Mid-task user messages to running agents +- Live SSE lifecycle stream (thoughts, tools, terminal, browser, usage) +- Process/container visibility and kill controls (with protection rules) +- Better completion artifacts and warnings (including missing PDF hints) +- OpenHands-style skill loading support +- RL integration scaffold and policy export helper - +### Latest Additions -## Installation +- `apply_patch_editor` as the primary code-editing tool for safer, atomic patch updates +- Live and final GitHub-style change summary (`files changed`, `+added`, `-deleted`) +- Richer tool execution cards with better status and file-change visibility +- Improved conversation reliability around long runs and follow-up continuity +- Runtime context observability (requested window, received window, usage ratio, auto-compress status) -We provide two installation methods. Method 2 (using uv) is recommended for faster installation and better dependency management. +## Architecture -### Method 1: Using conda +- Backend API: `server/api.py` (FastAPI + SSE) +- Worker runtime: `server/tasks.py` (Celery) +- Agent core: `app/agent/manus.py`, `app/agent/toolcall.py` +- LLM compatibility/runtime: `app/llm.py` +- Sandbox lifecycle: `app/sandbox/conversation.py` +- Frontend: `frontend/` (React + Vite) +- Persistence: PostgreSQL +- Event transport: Redis Streams -1. Create a new conda environment: +## Quick Start (Docker) -```bash -conda create -n open_manus python=3.12 -conda activate open_manus -``` - -2. Clone the repository: - -```bash -git clone https://github.com/FoundationAgents/OpenManus.git -cd OpenManus -``` - -3. Install dependencies: +1. Copy config: ```bash -pip install -r requirements.txt +cp config/config.example.toml config/config.toml ``` -### Method 2: Using uv (Recommended) +2. Set your model endpoint in `config/config.toml`. -1. Install uv (A fast Python package installer and resolver): +3. Start: ```bash -curl -LsSf https://astral.sh/uv/install.sh | sh +docker compose up --build ``` -2. Clone the repository: +Or use the project helper: ```bash -git clone https://github.com/FoundationAgents/OpenManus.git -cd OpenManus +make build ``` -3. Create a new virtual environment and activate it: +`make build` runs a clean Docker builder prune and then starts the stack with `docker compose up -d --build`. -```bash -uv venv --python 3.12 -source .venv/bin/activate # On Unix/macOS -# Or on Windows: -# .venv\Scripts\activate -``` +4. Open: -4. Install dependencies: +- Frontend: `http://localhost:3000` +- API: `http://localhost:8000` -```bash -uv pip install -r requirements.txt -``` +## Local Python Start -### Browser Automation Tool (Optional) ```bash -playwright install +uv venv --python 3.12 +source .venv/bin/activate +uv pip install -r requirements.txt +python main.py ``` ## Configuration -OpenManus requires configuration for the LLM APIs it uses. Follow these steps to set up your configuration: +Main file: `config/config.toml` -1. Create a `config.toml` file in the `config` directory (you can copy from the example): +Important sections: -```bash -cp config/config.example.toml config/config.toml -``` +- `[llm]` model, endpoint, key, token limits +- `[sandbox]` runtime limits and network/socket access +- `[agent]` max steps and tool-call behavior +- `[rl]` optional policy integration -2. Edit `config/config.toml` to add your API keys and customize settings: +Example RL toggle: ```toml -# Global LLM configuration -[llm] -model = "gpt-4o" -base_url = "https://api.openai.com/v1" -api_key = "sk-..." # Replace with your actual API key -max_tokens = 4096 -temperature = 0.0 - -# Optional configuration for specific LLM models -[llm.vision] -model = "gpt-4o" -base_url = "https://api.openai.com/v1" -api_key = "sk-..." # Replace with your actual API key +[rl] +enabled = true +policy_mode = "rl" +policy_path = "research/openmanus-rl/artifacts/policy/latest/policy.md" +metadata_path = "research/openmanus-rl/artifacts/policy/latest/metadata.json" ``` -## Quick Start - -One line for run OpenManus: +## RL Scaffold -```bash -python main.py -``` +This repo includes a clean path for OpenManus-RL style integration: -Then input your idea via terminal! +- `research/openmanus-rl/` +- `app/agent/policy_loader.py` +- `scripts/export_policy.py` -For MCP tool version, you can run: -```bash -python run_mcp.py -``` - -For unstable multi-agent version, you also can run: +Example: ```bash -python run_flow.py +python scripts/export_policy.py \ + --policy-file /path/to/policy.md \ + --model qwen3.6-35b-a3b \ + --benchmark gaia \ + --run-id exp-2026-05-14 ``` -### Custom Adding Multiple Agents +## Semantic Memory with FAISS -Currently, besides the general OpenManus Agent, we have also integrated the DataAnalysis Agent, which is suitable for data analysis and data visualization tasks. You can add this agent to `run_flow` in `config.toml`. +OpenManus features local, zero-dependency SQLite-based memory persistence (`AgentMemory`). You can optionally enable local FAISS vector semantic search to enhance memory recall when query wording differs from saved memories: ```toml -# Optional configuration for run-flow -[runflow] -use_data_analysis_agent = true # Disabled by default, change to true to activate +[agentmemory] +enabled = true +vector_backend = "faiss" # "none" or "faiss" +embedding_provider = "openai_compatible" +embedding_model = "text-embedding-nomic-embed-text-v1.5" +hybrid_search = true +vector_weight = 0.65 +keyword_weight = 0.35 ``` -In addition, you need to install the relevant dependencies to ensure the agent runs properly: [Detailed Installation Guide](app/tool/chart_visualization/README.md##Installation) -## How to contribute +When enabled, memories are indexed locally in FAISS alongside SQLite FTS5/BM25 keyword indices, returning weighted hybrid search results. If embedding generation or FAISS lookup fails, recall gracefully falls back to SQLite keyword search. -We welcome any friendly suggestions and helpful contributions! Just create issues or submit pull requests. +## DeepSpec Research Path -Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com +OpenManus includes an opt-in research integration scaffold for speculative decoding experiments using DeepSeek's DeepSpec framework: -**Note**: Before submitting a pull request, please use the pre-commit tool to check your changes. Run `pre-commit run --all-files` to execute the checks. +- See `research/deepspec/README.md` for workflow and evaluation instructions. +- Run `scripts/prepare_deepspec_research.sh` to clone or update the upstream repository. -## Community Group -Join our networking group on Feishu and share your experience with other developers! +**Note**: DeepSpec is strictly experimental and separate from standard runtime execution or default `make build` container generation. Target cache preparation requires high-capacity storage (e.g. ~38 TB for Qwen3-4B). -
- OpenManus 交流群 -
+## Contributing -## Star History +Contributions are very welcome. -[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date) +- Open an issue for bugs or feature proposals +- Keep PRs focused and easy to review +- Add verification steps in your PR description -## Sponsors -Thanks to [PPIO](https://ppinfra.com/user/register?invited_by=OCPKCN&utm_source=github_openmanus&utm_medium=github_readme&utm_campaign=link) for computing source support. -> PPIO: The most affordable and easily-integrated MaaS and GPU cloud solution. +### Maintainer Commitment +I actively monitor this repo and **I will respond to pull requests quickly**. +If your PR is blocked, tag me in the thread and I’ll help unblock it fast. -## Acknowledgement +### Suggested Team Flow -Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo), [browser-use](https://github.com/browser-use/browser-use) and [crawl4ai](https://github.com/unclecode/crawl4ai) for providing basic support for this project! +1. Create feature branches from `main` +2. Open PRs early (draft PRs are welcome) +3. Merge after review + verification -Additionally, we are grateful to [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT), [OpenHands](https://github.com/All-Hands-AI/OpenHands) and [SWE-agent](https://github.com/SWE-agent/SWE-agent). +## Runtime Notes -We also thank stepfun(阶跃星辰) for supporting our Hugging Face demo space. +- Sandboxes are scoped to conversation lifecycle. +- Protected system processes/containers are not killable by runtime controls. +- Deleting a conversation clears associated task records, streams, and sandbox state. -OpenManus is built by contributors from MetaGPT. Huge thanks to this agent community! +## License -## Cite -```bibtex -@misc{openmanus2025, - author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang and Bang Liu and Yuyu Luo and Chenglin Wu}, - title = {OpenManus: An open-source framework for building general AI agents}, - year = {2025}, - publisher = {Zenodo}, - doi = {10.5281/zenodo.15186407}, - url = {https://doi.org/10.5281/zenodo.15186407}, -} -``` +MIT (same as project root license). diff --git a/README_ja.md b/README_ja.md deleted file mode 100644 index d5fd281e5..000000000 --- a/README_ja.md +++ /dev/null @@ -1,193 +0,0 @@ -

- -

- -[English](README.md) | [中文](README_zh.md) | [한국어](README_ko.md) | 日本語 - -[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers) -  -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)   -[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z) -[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo) -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407) - -# 👋 OpenManus - -Manusは素晴らしいですが、OpenManusは*招待コード*なしでどんなアイデアも実現できます!🛫 - -私たちのチームメンバー [@Xinbin Liang](https://github.com/mannaandpoem) と [@Jinyu Xiang](https://github.com/XiangJinyu)(主要開発者)、そして [@Zhaoyang Yu](https://github.com/MoshiQAQ)、[@Jiayi Zhang](https://github.com/didiforgithub)、[@Sirui Hong](https://github.com/stellaHSR) は [@MetaGPT](https://github.com/geekan/MetaGPT) から来ました。プロトタイプは3時間以内に立ち上げられ、継続的に開発を進めています! - -これはシンプルな実装ですので、どんな提案、貢献、フィードバックも歓迎します! - -OpenManusで自分だけのエージェントを楽しみましょう! - -また、UIUCとOpenManusの研究者が共同開発した[OpenManus-RL](https://github.com/OpenManus/OpenManus-RL)をご紹介できることを嬉しく思います。これは強化学習(RL)ベース(GRPOなど)のLLMエージェントチューニング手法に特化したオープンソースプロジェクトです。 - -## プロジェクトデモ - - - -## インストール方法 - -インストール方法は2つ提供しています。方法2(uvを使用)は、より高速なインストールと優れた依存関係管理のため推奨されています。 - -### 方法1:condaを使用 - -1. 新しいconda環境を作成します: - -```bash -conda create -n open_manus python=3.12 -conda activate open_manus -``` - -2. リポジトリをクローンします: - -```bash -git clone https://github.com/FoundationAgents/OpenManus.git -cd OpenManus -``` - -3. 依存関係をインストールします: - -```bash -pip install -r requirements.txt -``` - -### 方法2:uvを使用(推奨) - -1. uv(高速なPythonパッケージインストーラーと管理機能)をインストールします: - -```bash -curl -LsSf https://astral.sh/uv/install.sh | sh -``` - -2. リポジトリをクローンします: - -```bash -git clone https://github.com/FoundationAgents/OpenManus.git -cd OpenManus -``` - -3. 新しい仮想環境を作成してアクティベートします: - -```bash -uv venv --python 3.12 -source .venv/bin/activate # Unix/macOSの場合 -# Windowsの場合: -# .venv\Scripts\activate -``` - -4. 依存関係をインストールします: - -```bash -uv pip install -r requirements.txt -``` - -### ブラウザ自動化ツール(オプション) -```bash -playwright install -``` - -## 設定 - -OpenManusを使用するには、LLM APIの設定が必要です。以下の手順に従って設定してください: - -1. `config`ディレクトリに`config.toml`ファイルを作成します(サンプルからコピーできます): - -```bash -cp config/config.example.toml config/config.toml -``` - -2. `config/config.toml`を編集してAPIキーを追加し、設定をカスタマイズします: - -```toml -# グローバルLLM設定 -[llm] -model = "gpt-4o" -base_url = "https://api.openai.com/v1" -api_key = "sk-..." # 実際のAPIキーに置き換えてください -max_tokens = 4096 -temperature = 0.0 - -# 特定のLLMモデル用のオプション設定 -[llm.vision] -model = "gpt-4o" -base_url = "https://api.openai.com/v1" -api_key = "sk-..." # 実際のAPIキーに置き換えてください -``` - -## クイックスタート - -OpenManusを実行する一行コマンド: - -```bash -python main.py -``` - -その後、ターミナルからプロンプトを入力してください! - -MCP ツールバージョンを使用する場合は、以下を実行します: -```bash -python run_mcp.py -``` - -開発中のマルチエージェントバージョンを試すには、以下を実行します: - -```bash -python run_flow.py -``` - -## カスタムマルチエージェントの追加 - -現在、一般的なOpenManusエージェントに加えて、データ分析とデータ可視化タスクに適したDataAnalysisエージェントが組み込まれています。このエージェントを`config.toml`の`run_flow`に追加することができます。 - -```toml -# run-flowのオプション設定 -[runflow] -use_data_analysis_agent = true # デフォルトでは無効、trueに変更すると有効化されます -``` - -これに加えて、エージェントが正常に動作するために必要な依存関係をインストールする必要があります:[具体的なインストールガイド](app/tool/chart_visualization/README_ja.md##インストール) - - -## 貢献方法 - -我々は建設的な意見や有益な貢献を歓迎します!issueを作成するか、プルリクエストを提出してください。 - -または @mannaandpoem に📧メールでご連絡ください:mannaandpoem@gmail.com - -**注意**: プルリクエストを送信する前に、pre-commitツールを使用して変更を確認してください。`pre-commit run --all-files`を実行してチェックを実行します。 - -## コミュニティグループ -Feishuのネットワーキンググループに参加して、他の開発者と経験を共有しましょう! - -
- OpenManus 交流群 -
- -## スター履歴 - -[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date) - -## 謝辞 - -このプロジェクトの基本的なサポートを提供してくれた[anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo) -と[browser-use](https://github.com/browser-use/browser-use)に感謝します! - -さらに、[AAAJ](https://github.com/metauto-ai/agent-as-a-judge)、[MetaGPT](https://github.com/geekan/MetaGPT)、[OpenHands](https://github.com/All-Hands-AI/OpenHands)、[SWE-agent](https://github.com/SWE-agent/SWE-agent)にも感謝します。 - -また、Hugging Face デモスペースをサポートしてくださった阶跃星辰 (stepfun)にも感謝いたします。 - -OpenManusはMetaGPTのコントリビューターによって構築されました。このエージェントコミュニティに大きな感謝を! - -## 引用 -```bibtex -@misc{openmanus2025, - author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang}, - title = {OpenManus: An open-source framework for building general AI agents}, - year = {2025}, - publisher = {Zenodo}, - doi = {10.5281/zenodo.15186407}, - url = {https://doi.org/10.5281/zenodo.15186407}, -} -``` diff --git a/README_ko.md b/README_ko.md deleted file mode 100644 index 5080f43e7..000000000 --- a/README_ko.md +++ /dev/null @@ -1,192 +0,0 @@ -

- -

- -[English](README.md) | [中文](README_zh.md) | 한국어 | [日本語](README_ja.md) - -[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers) -  -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)   -[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z) -[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo) -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407) - -# 👋 OpenManus - -Manus는 놀라운 도구지만, OpenManus는 *초대 코드* 없이도 모든 아이디어를 실현할 수 있습니다! 🛫 - -우리 팀의 멤버인 [@Xinbin Liang](https://github.com/mannaandpoem)와 [@Jinyu Xiang](https://github.com/XiangJinyu) (핵심 작성자), 그리고 [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), [@Sirui Hong](https://github.com/stellaHSR)이 함께 했습니다. 우리는 [@MetaGPT](https://github.com/geekan/MetaGPT)로부터 왔습니다. 프로토타입은 단 3시간 만에 출시되었으며, 계속해서 발전하고 있습니다! - -이 프로젝트는 간단한 구현에서 시작되었으며, 여러분의 제안, 기여 및 피드백을 환영합니다! - -OpenManus를 통해 여러분만의 에이전트를 즐겨보세요! - -또한 [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL)을 소개하게 되어 기쁩니다. OpenManus와 UIUC 연구자들이 공동 개발한 이 오픈소스 프로젝트는 LLM 에이전트에 대해 강화 학습(RL) 기반 (예: GRPO) 튜닝 방법을 제공합니다. - -## 프로젝트 데모 - - - -## 설치 방법 - -두 가지 설치 방법을 제공합니다. **방법 2 (uv 사용)** 이 더 빠른 설치와 효율적인 종속성 관리를 위해 권장됩니다. - -### 방법 1: conda 사용 - -1. 새로운 conda 환경을 생성합니다: - -```bash -conda create -n open_manus python=3.12 -conda activate open_manus -``` - -2. 저장소를 클론합니다: - -```bash -git clone https://github.com/FoundationAgents/OpenManus.git -cd OpenManus -``` - -3. 종속성을 설치합니다: - -```bash -pip install -r requirements.txt -``` - -### 방법 2: uv 사용 (권장) - -1. uv를 설치합니다. (빠른 Python 패키지 설치 및 종속성 관리 도구): - -```bash -curl -LsSf https://astral.sh/uv/install.sh | sh -``` - -2. 저장소를 클론합니다: - -```bash -git clone https://github.com/FoundationAgents/OpenManus.git -cd OpenManus -``` - -3. 새로운 가상 환경을 생성하고 활성화합니다: - -```bash -uv venv --python 3.12 -source .venv/bin/activate # Unix/macOS의 경우 -# Windows의 경우: -# .venv\Scripts\activate -``` - -4. 종속성을 설치합니다: - -```bash -uv pip install -r requirements.txt -``` - -### 브라우저 자동화 도구 (선택사항) -```bash -playwright install -``` - -## 설정 방법 - -OpenManus를 사용하려면 사용하는 LLM API에 대한 설정이 필요합니다. 아래 단계를 따라 설정을 완료하세요: - -1. `config` 디렉토리에 `config.toml` 파일을 생성하세요 (예제 파일을 복사하여 사용할 수 있습니다): - -```bash -cp config/config.example.toml config/config.toml -``` - -2. `config/config.toml` 파일을 편집하여 API 키를 추가하고 설정을 커스터마이징하세요: - -```toml -# 전역 LLM 설정 -[llm] -model = "gpt-4o" -base_url = "https://api.openai.com/v1" -api_key = "sk-..." # 실제 API 키로 변경하세요 -max_tokens = 4096 -temperature = 0.0 - -# 특정 LLM 모델에 대한 선택적 설정 -[llm.vision] -model = "gpt-4o" -base_url = "https://api.openai.com/v1" -api_key = "sk-..." # 실제 API 키로 변경하세요 -``` - -## 빠른 시작 - -OpenManus를 실행하는 한 줄 명령어: - -```bash -python main.py -``` - -이후 터미널에서 아이디어를 작성하세요! - -MCP 도구 버전을 사용하려면 다음을 실행하세요: -```bash -python run_mcp.py -``` - -불안정한 멀티 에이전트 버전을 실행하려면 다음을 실행할 수 있습니다: - -```bash -python run_flow.py -``` - -### 사용자 정의 다중 에이전트 추가 - -현재 일반 OpenManus 에이전트 외에도 데이터 분석 및 데이터 시각화 작업에 적합한 DataAnalysis 에이전트를 통합했습니다. 이 에이전트를 `config.toml`의 `run_flow`에 추가할 수 있습니다. - -```toml -# run-flow에 대한 선택적 구성 -[runflow] -use_data_analysis_agent = true # 기본적으로 비활성화되어 있으며, 활성화하려면 true로 변경 -``` - -또한, 에이전트가 제대로 작동하도록 관련 종속성을 설치해야 합니다: [상세 설치 가이드](app/tool/chart_visualization/README.md##Installation) - -## 기여 방법 - -모든 친절한 제안과 유용한 기여를 환영합니다! 이슈를 생성하거나 풀 리퀘스트를 제출해 주세요. - -또는 📧 메일로 연락주세요. @mannaandpoem : mannaandpoem@gmail.com - -**참고**: pull request를 제출하기 전에 pre-commit 도구를 사용하여 변경 사항을 확인하십시오. `pre-commit run --all-files`를 실행하여 검사를 실행합니다. - -## 커뮤니티 그룹 -Feishu 네트워킹 그룹에 참여하여 다른 개발자들과 경험을 공유하세요! - -
- OpenManus 交流群 -
- -## Star History - -[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date) - -## 감사의 글 - -이 프로젝트에 기본적인 지원을 제공해 주신 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)와 -[browser-use](https://github.com/browser-use/browser-use)에게 감사드립니다! - -또한, [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT), [OpenHands](https://github.com/All-Hands-AI/OpenHands), [SWE-agent](https://github.com/SWE-agent/SWE-agent)에 깊은 감사를 드립니다. - -또한 Hugging Face 데모 공간을 지원해 주신 阶跃星辰 (stepfun)에게 감사드립니다. - -OpenManus는 MetaGPT 기여자들에 의해 개발되었습니다. 이 에이전트 커뮤니티에 깊은 감사를 전합니다! - -## 인용 -```bibtex -@misc{openmanus2025, - author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang}, - title = {OpenManus: An open-source framework for building general AI agents}, - year = {2025}, - publisher = {Zenodo}, - doi = {10.5281/zenodo.15186407}, - url = {https://doi.org/10.5281/zenodo.15186407}, -} -``` diff --git a/README_zh.md b/README_zh.md deleted file mode 100644 index 680ccc1d9..000000000 --- a/README_zh.md +++ /dev/null @@ -1,198 +0,0 @@ -

- -

- -[English](README.md) | 中文 | [한국어](README_ko.md) | [日本語](README_ja.md) - -[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers) -  -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)   -[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z) -[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo) -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407) - -# 👋 OpenManus - -Manus 非常棒,但 OpenManus 无需邀请码即可实现任何创意 🛫! - -我们的团队成员 [@Xinbin Liang](https://github.com/mannaandpoem) 和 [@Jinyu Xiang](https://github.com/XiangJinyu)(核心作者),以及 [@Zhaoyang Yu](https://github.com/MoshiQAQ)、[@Jiayi Zhang](https://github.com/didiforgithub) 和 [@Sirui Hong](https://github.com/stellaHSR),来自 [@MetaGPT](https://github.com/geekan/MetaGPT)团队。我们在 3 -小时内完成了开发并持续迭代中! - -这是一个简洁的实现方案,欢迎任何建议、贡献和反馈! - -用 OpenManus 开启你的智能体之旅吧! - -我们也非常高兴地向大家介绍 [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL),这是一个专注于基于强化学习(RL,例如 GRPO)的方法来优化大语言模型(LLM)智能体的开源项目,由来自UIUC 和 OpenManus 的研究人员合作开发。 - -## 项目演示 - - - -## 安装指南 - -我们提供两种安装方式。推荐使用方式二(uv),因为它能提供更快的安装速度和更好的依赖管理。 - -### 方式一:使用 conda - -1. 创建新的 conda 环境: - -```bash -conda create -n open_manus python=3.12 -conda activate open_manus -``` - -2. 克隆仓库: - -```bash -git clone https://github.com/FoundationAgents/OpenManus.git -cd OpenManus -``` - -3. 安装依赖: - -```bash -pip install -r requirements.txt -``` - -### 方式二:使用 uv(推荐) - -1. 安装 uv(一个快速的 Python 包管理器): - -```bash -curl -LsSf https://astral.sh/uv/install.sh | sh -``` - -2. 克隆仓库: - -```bash -git clone https://github.com/FoundationAgents/OpenManus.git -cd OpenManus -``` - -3. 创建并激活虚拟环境: - -```bash -uv venv --python 3.12 -source .venv/bin/activate # Unix/macOS 系统 -# Windows 系统使用: -# .venv\Scripts\activate -``` - -4. 安装依赖: - -```bash -uv pip install -r requirements.txt -``` - -### 浏览器自动化工具(可选) -```bash -playwright install -``` - -## 配置说明 - -OpenManus 需要配置使用的 LLM API,请按以下步骤设置: - -1. 在 `config` 目录创建 `config.toml` 文件(可从示例复制): - -```bash -cp config/config.example.toml config/config.toml -``` - -2. 编辑 `config/config.toml` 添加 API 密钥和自定义设置: - -```toml -# 全局 LLM 配置 -[llm] -model = "gpt-4o" -base_url = "https://api.openai.com/v1" -api_key = "sk-..." # 替换为真实 API 密钥 -max_tokens = 4096 -temperature = 0.0 - -# 可选特定 LLM 模型配置 -[llm.vision] -model = "gpt-4o" -base_url = "https://api.openai.com/v1" -api_key = "sk-..." # 替换为真实 API 密钥 -``` - -## 快速启动 - -一行命令运行 OpenManus: - -```bash -python main.py -``` - -然后通过终端输入你的创意! - -如需使用 MCP 工具版本,可运行: -```bash -python run_mcp.py -``` - -如需体验不稳定的多智能体版本,可运行: - -```bash -python run_flow.py -``` - -## 添加自定义多智能体 - -目前除了通用的 OpenManus Agent, 我们还内置了DataAnalysis Agent,适用于数据分析和数据可视化任务,你可以在`config.toml`中将这个智能体加入到`run_flow`中 -```toml -# run-flow可选配置 -[runflow] -use_data_analysis_agent = true # 默认关闭,将其改为true则为激活 -``` -除此之外,你还需要安装相关的依赖来确保智能体正常运行:[具体安装指南](app/tool/chart_visualization/README_zh.md##安装) - - -## 贡献指南 - -我们欢迎任何友好的建议和有价值的贡献!可以直接创建 issue 或提交 pull request。 - -或通过 📧 邮件联系 @mannaandpoem:mannaandpoem@gmail.com - -**注意**: 在提交 pull request 之前,请使用 pre-commit 工具检查您的更改。运行 `pre-commit run --all-files` 来执行检查。 - -## 交流群 - -加入我们的飞书交流群,与其他开发者分享经验! - -
- OpenManus 交流群 -
- -## Star 数量 - -[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date) - - -## 赞助商 -感谢[PPIO](https://ppinfra.com/user/register?invited_by=OCPKCN&utm_source=github_openmanus&utm_medium=github_readme&utm_campaign=link) 提供的算力支持。 -> PPIO派欧云:一键调用高性价比的开源模型API和GPU容器 - -## 致谢 - -特别感谢 [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo) -和 [browser-use](https://github.com/browser-use/browser-use) 为本项目提供的基础支持! - -此外,我们感谢 [AAAJ](https://github.com/metauto-ai/agent-as-a-judge),[MetaGPT](https://github.com/geekan/MetaGPT),[OpenHands](https://github.com/All-Hands-AI/OpenHands) 和 [SWE-agent](https://github.com/SWE-agent/SWE-agent). - -我们也感谢阶跃星辰 (stepfun) 提供的 Hugging Face 演示空间支持。 - -OpenManus 由 MetaGPT 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区! - -## 引用 -```bibtex -@misc{openmanus2025, - author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang}, - title = {OpenManus: An open-source framework for building general AI agents}, - year = {2025}, - publisher = {Zenodo}, - doi = {10.5281/zenodo.15186407}, - url = {https://doi.org/10.5281/zenodo.15186407}, -} -``` diff --git a/app/agent/__init__.py b/app/agent/__init__.py index f7df2b9ba..0e67b0651 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -1,6 +1,8 @@ from app.agent.base import BaseAgent from app.agent.browser import BrowserAgent +from app.agent.executor import ExecutorAgent from app.agent.mcp import MCPAgent +from app.agent.planner import PlannerAgent from app.agent.react import ReActAgent from app.agent.swe import SWEAgent from app.agent.toolcall import ToolCallAgent @@ -13,4 +15,6 @@ "SWEAgent", "ToolCallAgent", "MCPAgent", + "PlannerAgent", + "ExecutorAgent", ] diff --git a/app/agent/base.py b/app/agent/base.py index 65f660073..705a44ad2 100644 --- a/app/agent/base.py +++ b/app/agent/base.py @@ -1,13 +1,18 @@ from abc import ABC, abstractmethod from contextlib import asynccontextmanager -from typing import List, Optional +from typing import Any, List, Optional from pydantic import BaseModel, Field, model_validator +from app.config import config from app.llm import LLM -from app.logger import logger from app.sandbox.client import SANDBOX_CLIENT from app.schema import ROLE_TYPE, AgentState, Memory, Message +from core.task import Task + + +class TaskInterrupted(Exception): + """Raised when a task is interrupted.""" class BaseAgent(BaseModel, ABC): @@ -37,7 +42,9 @@ class BaseAgent(BaseModel, ABC): ) # Execution control - max_steps: int = Field(default=10, description="Maximum steps before termination") + max_steps: int = Field( + default=config.agent.max_steps, description="Maximum steps before termination" + ) current_step: int = Field(default=0, description="Current step in execution") duplicate_threshold: int = 2 @@ -113,76 +120,134 @@ def update_memory( kwargs = {"base64_image": base64_image, **(kwargs if role == "tool" else {})} self.memory.add_message(message_map[role](content, **kwargs)) - async def run(self, request: Optional[str] = None) -> str: - """Execute the agent's main loop asynchronously. + async def run(self, task: Task, input: Any) -> str: + """Execute the agent's main loop asynchronously.""" + if task.is_interrupted(): + raise TaskInterrupted() - Args: - request: Optional initial user request to process. - - Returns: - A string summarizing the execution results. - - Raises: - RuntimeError: If the agent is not in IDLE state at start. - """ if self.state != AgentState.IDLE: raise RuntimeError(f"Cannot run agent from state: {self.state}") - if request: - self.update_memory("user", request) + if input is not None: + self.update_memory("user", str(input)) results: List[str] = [] - async with self.state_context(AgentState.RUNNING): - while ( - self.current_step < self.max_steps and self.state != AgentState.FINISHED - ): - self.current_step += 1 - logger.info(f"Executing step {self.current_step}/{self.max_steps}") - step_result = await self.step() - - # Check for stuck state - if self.is_stuck(): - self.handle_stuck_state() - - results.append(f"Step {self.current_step}: {step_result}") - - if self.current_step >= self.max_steps: - self.current_step = 0 - self.state = AgentState.IDLE - results.append(f"Terminated: Reached max steps ({self.max_steps})") - await SANDBOX_CLIENT.cleanup() - return "\n".join(results) if results else "No steps executed" + try: + async with self.state_context(AgentState.RUNNING): + task.emit( + "agent_state", + {"state": "running", "agent": self.name}, + ) + while ( + self.current_step < self.max_steps + and self.state != AgentState.FINISHED + ): + if task.is_interrupted(): + raise TaskInterrupted() + + self.current_step += 1 + task.emit( + "step_start", + {"step": self.current_step, "max_steps": self.max_steps}, + ) + step_result = await self.step(task) + + if self.is_stuck(): + self.handle_stuck_state(task) + + results.append(f"Step {self.current_step}: {step_result}") + task.emit( + "step_result", + {"step": self.current_step, "result": step_result}, + ) + + if self.current_step >= self.max_steps: + self.current_step = 0 + self.state = AgentState.IDLE + termination_msg = ( + f"Terminated: Reached max steps ({self.max_steps})" + ) + results.append(termination_msg) + task.emit( + "terminated", + { + "reason": termination_msg, + "status": "stuck", + "message": "Agent stopped because it reached the configured step limit.", + }, + ) + return "\n".join(results) if results else "No steps executed" + finally: + task.emit( + "agent_state", + { + "state": str( + self.state.value if hasattr(self.state, "value") else self.state + ), + "agent": self.name, + }, + ) + await SANDBOX_CLIENT.cleanup() @abstractmethod - async def step(self) -> str: + async def step(self, task: Task) -> str: """Execute a single step in the agent's workflow. Must be implemented by subclasses to define specific behavior. """ - def handle_stuck_state(self): + def handle_stuck_state(self, task: Task): """Handle stuck state by adding a prompt to change strategy""" stuck_prompt = "\ Observed duplicate responses. Consider new strategies and avoid repeating ineffective paths already attempted." self.next_step_prompt = f"{stuck_prompt}\n{self.next_step_prompt}" - logger.warning(f"Agent detected stuck state. Added prompt: {stuck_prompt}") + task.emit( + "stuck_detected", + { + "state": "stuck", + "message": "Agent detected repeated responses and injected a strategy-change prompt.", + }, + ) def is_stuck(self) -> bool: - """Check if the agent is stuck in a loop by detecting duplicate content""" - if len(self.memory.messages) < 2: + """Detect stuck loops via two complementary signals. + + 1. Semantic tool-loop: the same tool is called with identical arguments + three or more times in the last 12 assistant messages — even if the + surrounding text is different each time. + 2. Content-duplicate fallback: the full assistant content matches + exactly `duplicate_threshold` times in recent history (original + behaviour, kept as a safety net). + """ + messages = self.memory.messages + if len(messages) < 2: return False - last_message = self.memory.messages[-1] + # --- Signal 1: repeated tool-call signatures --- + recent_with_tools = [ + m for m in messages[-12:] if getattr(m, "tool_calls", None) + ] + if len(recent_with_tools) >= self.duplicate_threshold: + call_signatures: list[str] = [] + for msg in recent_with_tools: + for tc in msg.tool_calls or []: + sig = f"{tc.function.name}:{tc.function.arguments}" + call_signatures.append(sig) + from collections import Counter + + counts = Counter(call_signatures) + if any(count >= self.duplicate_threshold for count in counts.values()): + return True + + # --- Signal 2: exact content duplicate (original fallback) --- + last_message = messages[-1] if not last_message.content: return False - - # Count identical content occurrences duplicate_count = sum( 1 - for msg in reversed(self.memory.messages[:-1]) + for msg in reversed(messages[:-1]) if msg.role == "assistant" and msg.content == last_message.content ) - return duplicate_count >= self.duplicate_threshold @property diff --git a/app/agent/browser.py b/app/agent/browser.py index 3f25c4539..e77fe63e3 100644 --- a/app/agent/browser.py +++ b/app/agent/browser.py @@ -3,8 +3,8 @@ from pydantic import Field, model_validator +from app.agent.base import Task, TaskInterrupted from app.agent.toolcall import ToolCallAgent -from app.logger import logger from app.prompt.browser import NEXT_STEP_PROMPT, SYSTEM_PROMPT from app.schema import Message, ToolChoice from app.tool import BrowserUseTool, Terminate, ToolCollection @@ -28,20 +28,17 @@ async def get_browser_state(self) -> Optional[dict]: SandboxBrowserTool().name ) if not browser_tool or not hasattr(browser_tool, "get_current_state"): - logger.warning("BrowserUseTool not found or doesn't have get_current_state") return None try: result = await browser_tool.get_current_state() if result.error: - logger.debug(f"Browser state error: {result.error}") return None if hasattr(result, "base64_image") and result.base64_image: self._current_base64_image = result.base64_image else: self._current_base64_image = None return json.loads(result.output) - except Exception as e: - logger.debug(f"Failed to get browser state: {str(e)}") + except Exception: return None async def format_next_step_prompt(self) -> str: @@ -117,12 +114,14 @@ def initialize_helper(self) -> "BrowserAgent": self.browser_context_helper = BrowserContextHelper(self) return self - async def think(self) -> bool: + async def think(self, task: Task) -> bool: """Process current state and decide next actions using tools, with browser state info added""" + if task.is_interrupted(): + raise TaskInterrupted() self.next_step_prompt = ( await self.browser_context_helper.format_next_step_prompt() ) - return await super().think() + return await super().think(task) async def cleanup(self): """Clean up browser agent resources by calling parent cleanup.""" diff --git a/app/agent/executor.py b/app/agent/executor.py new file mode 100644 index 000000000..781854216 --- /dev/null +++ b/app/agent/executor.py @@ -0,0 +1,101 @@ +import json +from typing import Any, Dict, List, Union + +from app.agent.base import Task, TaskInterrupted +from app.agent.toolcall import ToolCallAgent + + +class ExecutorAgent(ToolCallAgent): + """Executor that consumes a planner-produced plan and runs its steps with tools.""" + + name: str = "executor" + description: str = "Executes structured plan steps and emits execution events." + + async def run(self, task: Task, plan: Any) -> str: + if task.is_interrupted(): + raise TaskInterrupted() + + steps = self._normalize_plan(plan) + results: List[str] = [] + + for idx, step in enumerate(steps): + if task.is_interrupted(): + raise TaskInterrupted() + + task.emit("execute.step.start", {"index": idx, "step": step}) + step_prompt = self._format_step_prompt(step, idx) + result = await super().run(task, step_prompt) + results.append(result) + + return "\n".join(results) + + def _normalize_plan(self, plan: Any) -> List[Dict[str, Any]]: + """Accept list/dict/str plan and normalize to list of dict steps.""" + if isinstance(plan, list): + return [self._ensure_dict(step, i) for i, step in enumerate(plan)] + if isinstance(plan, dict): + return [self._ensure_dict(plan, 0)] + if isinstance(plan, str): + try: + data = json.loads(plan) + if isinstance(data, list): + return [self._ensure_dict(s, i) for i, s in enumerate(data)] + if isinstance(data, dict): + return [self._ensure_dict(data, 0)] + except json.JSONDecodeError: + pass + # fallback single step + return [ + { + "id": "step-1", + "title": "Execute task", + "action": str(plan), + "expected_result": "Task completed", + } + ] + + def _ensure_dict(self, step: Any, idx: int) -> Dict[str, Any]: + if isinstance(step, dict): + if "id" not in step: + step = {**step, "id": step.get("title") or f"step-{idx+1}"} + return step + return { + "id": f"step-{idx+1}", + "title": "Execute step", + "action": str(step), + "expected_result": "", + } + + def _format_step_prompt(self, step: Dict[str, Any], idx: int) -> str: + title = step.get("title") or f"Step {idx+1}" + action = step.get("action") or "" + expected = step.get("expected_result") or "" + return ( + f"Step {idx+1}: {title}\n" + f"Action: {action}\n" + f"Expected Result: {expected}\n" + "Execute this step using available tools. Return a concise result." + ) + + async def execute_tool(self, command, task: Task) -> str: # type: ignore[override] + if task.is_interrupted(): + raise TaskInterrupted() + + name = getattr(getattr(command, "function", None), "name", None) + raw_args = getattr(getattr(command, "function", None), "arguments", None) + parsed_args: Union[dict, str, None] + try: + parsed_args = json.loads(raw_args or "{}") + except Exception: + parsed_args = raw_args + + if name: + task.emit("tool.call", {"tool": name, "args": parsed_args}) + + result = await super().execute_tool(command, task) + + task.emit("tool.result", {"tool": name, "result": result}) + return result + + +__all__ = ["ExecutorAgent"] diff --git a/app/agent/manus.py b/app/agent/manus.py index df40edbba..ec1c4044a 100644 --- a/app/agent/manus.py +++ b/app/agent/manus.py @@ -2,17 +2,33 @@ from pydantic import Field, model_validator +from app.agent.base import Task, TaskInterrupted from app.agent.browser import BrowserContextHelper +from app.agent.policy_loader import load_rl_policy_context from app.agent.toolcall import ToolCallAgent from app.config import config -from app.logger import logger from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT -from app.tool import Terminate, ToolCollection +from app.tool import ( + ApplyPatchEditor, + Bash, + CodebaseOverview, + GlobSearch, + GrepSearch, + LineEdit, + MemoryRecall, + MemorySave, + PlanningTool, + ReadFiles, + SkillPlaybook, + Terminate, + ToolCollection, + WaitForUserInput, + WebSearch, +) from app.tool.ask_human import AskHuman from app.tool.browser_use_tool import BrowserUseTool from app.tool.mcp import MCPClients, MCPClientTool from app.tool.python_execute import PythonExecute -from app.tool.str_replace_editor import StrReplaceEditor class Manus(ToolCallAgent): @@ -23,9 +39,11 @@ class Manus(ToolCallAgent): system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root) next_step_prompt: str = NEXT_STEP_PROMPT + workspace_root: str = str(config.workspace_root) + disabled_tools: set[str] = Field(default_factory=set) max_observe: int = 10000 - max_steps: int = 20 + max_steps: int = config.agent.max_steps # MCP clients for remote tool access mcp_clients: MCPClients = Field(default_factory=MCPClients) @@ -33,10 +51,22 @@ class Manus(ToolCallAgent): # Add general-purpose tools to the tool collection available_tools: ToolCollection = Field( default_factory=lambda: ToolCollection( + SkillPlaybook(), + PlanningTool(), + CodebaseOverview(), + GlobSearch(), + GrepSearch(), + ReadFiles(), PythonExecute(), + Bash(), BrowserUseTool(), - StrReplaceEditor(), + WebSearch(), + LineEdit(), # primary code editor — line-number based, no string matching + ApplyPatchEditor(), # multi-file atomic patches + MemorySave(), + MemoryRecall(), AskHuman(), + WaitForUserInput(), Terminate(), ) ) @@ -53,6 +83,12 @@ class Manus(ToolCallAgent): @model_validator(mode="after") def initialize_helper(self) -> "Manus": """Initialize basic components synchronously.""" + self.system_prompt = SYSTEM_PROMPT.format(directory=self.workspace_root) + rl_context = load_rl_policy_context() + if rl_context: + self.system_prompt = f"{self.system_prompt}\n\n{rl_context}" + if self.disabled_tools: + self.available_tools = self.available_tools.without(self.disabled_tools) self.browser_context_helper = BrowserContextHelper(self) return self @@ -71,9 +107,6 @@ async def initialize_mcp_servers(self) -> None: if server_config.type == "sse": if server_config.url: await self.connect_mcp_server(server_config.url, server_id) - logger.info( - f"Connected to MCP server {server_id} at {server_config.url}" - ) elif server_config.type == "stdio": if server_config.command: await self.connect_mcp_server( @@ -82,11 +115,8 @@ async def initialize_mcp_servers(self) -> None: use_stdio=True, stdio_args=server_config.args, ) - logger.info( - f"Connected to MCP server {server_id} using command {server_config.command}" - ) except Exception as e: - logger.error(f"Failed to connect to MCP server {server_id}: {e}") + _ = e async def connect_mcp_server( self, @@ -137,8 +167,10 @@ async def cleanup(self): await self.disconnect_mcp_server() self._initialized = False - async def think(self) -> bool: + async def think(self, task: Task) -> bool: """Process current state and decide next actions with appropriate context.""" + if task.is_interrupted(): + raise TaskInterrupted() if not self._initialized: await self.initialize_mcp_servers() self._initialized = True @@ -157,7 +189,7 @@ async def think(self) -> bool: await self.browser_context_helper.format_next_step_prompt() ) - result = await super().think() + result = await super().think(task) # Restore original prompt self.next_step_prompt = original_prompt diff --git a/app/agent/mcp.py b/app/agent/mcp.py index 9c6da6aaa..3531cc230 100644 --- a/app/agent/mcp.py +++ b/app/agent/mcp.py @@ -2,8 +2,8 @@ from pydantic import Field +from app.agent.base import Task, TaskInterrupted from app.agent.toolcall import ToolCallAgent -from app.logger import logger from app.prompt.mcp import MULTIMEDIA_RESPONSE_PROMPT, NEXT_STEP_PROMPT, SYSTEM_PROMPT from app.schema import AgentState, Message from app.tool.base import ToolResult @@ -84,7 +84,7 @@ async def initialize( ) ) - async def _refresh_tools(self) -> Tuple[List[str], List[str]]: + async def _refresh_tools(self, task: Task) -> Tuple[List[str], List[str]]: """Refresh the list of available tools from the MCP server. Returns: @@ -113,48 +113,56 @@ async def _refresh_tools(self) -> Tuple[List[str], List[str]]: # Update stored schemas self.tool_schemas = current_tools - # Log and notify about changes if added_tools: - logger.info(f"Added MCP tools: {added_tools}") + task.emit("tools_added", {"tools": added_tools}) self.memory.add_message( Message.system_message(f"New tools available: {', '.join(added_tools)}") ) if removed_tools: - logger.info(f"Removed MCP tools: {removed_tools}") + task.emit("tools_removed", {"tools": removed_tools}) self.memory.add_message( Message.system_message( f"Tools no longer available: {', '.join(removed_tools)}" ) ) if changed_tools: - logger.info(f"Changed MCP tools: {changed_tools}") + task.emit("tools_changed", {"tools": changed_tools}) return added_tools, removed_tools - async def think(self) -> bool: + async def think(self, task: Task) -> bool: """Process current state and decide next action.""" + if task.is_interrupted(): + raise TaskInterrupted() + # Check MCP session and tools availability if not self.mcp_clients.sessions or not self.mcp_clients.tool_map: - logger.info("MCP service is no longer available, ending interaction") + task.emit( + "info", {"message": "MCP service is no longer available, ending run."} + ) self.state = AgentState.FINISHED return False # Refresh tools periodically if self.current_step % self._refresh_tools_interval == 0: - await self._refresh_tools() + await self._refresh_tools(task) # All tools removed indicates shutdown if not self.mcp_clients.tool_map: - logger.info("MCP service has shut down, ending interaction") + task.emit("info", {"message": "MCP service has shut down, ending run."}) self.state = AgentState.FINISHED return False # Use the parent class's think method - return await super().think() + return await super().think(task) - async def _handle_special_tool(self, name: str, result: Any, **kwargs) -> None: + async def _handle_special_tool( + self, task: Task, name: str, result: Any, **kwargs + ) -> None: """Handle special tool execution and state changes""" # First process with parent handler - await super()._handle_special_tool(name, result, **kwargs) + await super()._handle_special_tool( + task=task, name=name, result=result, **kwargs + ) # Handle multimedia responses if isinstance(result, ToolResult) and result.base64_image: @@ -173,13 +181,12 @@ async def cleanup(self) -> None: """Clean up MCP connection when done.""" if self.mcp_clients.sessions: await self.mcp_clients.disconnect() - logger.info("MCP connection closed") + # No external logging; silent cleanup - async def run(self, request: Optional[str] = None) -> str: + async def run(self, task: Task, input: Optional[str] = None) -> str: """Run the agent with cleanup when done.""" try: - result = await super().run(request) + result = await super().run(task, input) return result finally: - # Ensure cleanup happens even if there's an error await self.cleanup() diff --git a/app/agent/planner.py b/app/agent/planner.py new file mode 100644 index 000000000..b36ac9848 --- /dev/null +++ b/app/agent/planner.py @@ -0,0 +1,125 @@ +import json +from typing import Any, Dict, List, Optional + +from pydantic import Field + +from app.agent.base import BaseAgent, Task, TaskInterrupted +from app.prompt.planning import PLANNING_SYSTEM_PROMPT +from app.schema import Message +from context.engine import ContextEngine + + +class PlannerAgent(BaseAgent): + """Lightweight planner that produces a structured plan without executing tools.""" + + name: str = "planner" + description: str = ( + "Generates structured plans (list[dict]) and emits plan events via task." + ) + + system_prompt: str = PLANNING_SYSTEM_PROMPT + next_step_prompt: Optional[str] = None + + max_steps: int = 1 + current_step: int = 0 + + plan_fields: List[str] = Field( + default_factory=lambda: ["id", "title", "action", "expected_result"] + ) + + async def run(self, task: Task, input: Any) -> List[Dict[str, Any]]: + if task.is_interrupted(): + raise TaskInterrupted() + + request = "" if input is None else str(input).strip() + plan = await self._generate_plan(task, request) + + for idx, step in enumerate(plan): + task.emit("plan.step", {"index": idx, "step": step}) + + task.emit("plan.done", {"steps": len(plan), "plan": plan}) + return plan + + async def _generate_plan(self, task: Task, request: str) -> List[Dict[str, Any]]: + """Use the existing LLM to create a structured plan without calling tools.""" + if task.is_interrupted(): + raise TaskInterrupted() + + if not request: + default_plan = [ + { + "id": "step-1", + "title": "No request provided", + "action": "Await valid task input", + "expected_result": "Receive task details to plan", + } + ] + return default_plan + + user_prompt = ( + "Create a concise, actionable plan as a JSON array. " + "Each item must be an object with keys: " + f"{', '.join(self.plan_fields)}. " + "Keep 3-7 steps, ordered, no prose outside JSON." + f"\n\nTask: {request}" + ) + + context = ContextEngine.build(task, agent_role=self.name, step_type="plan") + ctx_msg = Message.system_message(json.dumps(context, ensure_ascii=False)) + + response = await self.llm.ask( + messages=[Message.user_message(user_prompt)], + system_msgs=[Message.system_message(self.system_prompt), ctx_msg], + ) + + plan = self._parse_plan(response) + if not plan: + plan = [ + { + "id": "step-1", + "title": "Analyze task", + "action": f"Understand requirements: {request}", + "expected_result": "Clear scope and constraints", + }, + { + "id": "step-2", + "title": "Execute task", + "action": "Perform required actions to complete the task", + "expected_result": "Task objectives met", + }, + { + "id": "step-3", + "title": "Verify results", + "action": "Validate outputs and summarize findings", + "expected_result": "Confirmed completion with summary", + }, + ] + return plan + + def _parse_plan(self, text: str) -> List[Dict[str, Any]]: + """Parse JSON plan and normalize fields.""" + if not text: + return [] + try: + data = json.loads(text) + except json.JSONDecodeError: + return [] + + if not isinstance(data, list): + return [] + + normalized: List[Dict[str, Any]] = [] + for i, item in enumerate(data): + if not isinstance(item, dict): + continue + step = {field: item.get(field) for field in self.plan_fields} + # Fallback IDs if missing + if not step.get("id"): + step["id"] = f"step-{i+1}" + if not step.get("title") and step.get("action"): + step["title"] = step["action"] + normalized.append(step) + return normalized + + async def step(self, task: Task) -> str: # pragma: no cover - run overrides loop + raise NotImplementedError("PlannerAgent does not use step-based execution") diff --git a/app/agent/policy_loader.py b/app/agent/policy_loader.py new file mode 100644 index 000000000..62e1f746b --- /dev/null +++ b/app/agent/policy_loader.py @@ -0,0 +1,55 @@ +import json +from pathlib import Path +from typing import Optional + +from app.config import config + + +def _read_text(path: Path) -> Optional[str]: + try: + if not path.exists(): + return None + return path.read_text(encoding="utf-8").strip() + except OSError: + return None + + +def _read_json(path: Path) -> Optional[dict]: + try: + if not path.exists(): + return None + return json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return None + + +def load_rl_policy_context() -> str: + """Return an optional RL policy context block for Manus. + + This hook is intentionally lightweight: if policy files are missing, the + default behavior remains unchanged. + """ + if not config.rl.enabled or config.rl.policy_mode.lower() != "rl": + return "" + + root = config.root_path + policy_path = root / config.rl.policy_path + metadata_path = root / config.rl.metadata_path + + policy_text = _read_text(policy_path) + if not policy_text: + return "" + + metadata = _read_json(metadata_path) or {} + benchmark = metadata.get("benchmark", "unknown") + model_name = metadata.get("model", "unknown") + run_id = metadata.get("run_id", "unknown") + + return ( + "RL policy context is enabled for this run.\n" + f"- Benchmark source: {benchmark}\n" + f"- Tuned model family: {model_name}\n" + f"- Policy run id: {run_id}\n\n" + "Policy guidance:\n" + f"{policy_text}" + ) diff --git a/app/agent/react.py b/app/agent/react.py index 7f9482082..a45cbd000 100644 --- a/app/agent/react.py +++ b/app/agent/react.py @@ -1,9 +1,21 @@ +"""ReActAgent — Think → Act → Observe loop with explicit structured trace events. + +Every step emits three lifecycle events so the UI and logs have a complete, +structured trace of the agent's reasoning: + + agent:lifecycle:step:reason — the LLM's textual reasoning (thought) + agent:lifecycle:step:act — what tools were decided and dispatched + agent:lifecycle:step:observe — summary of the tool results (observation) + +These map directly to the classic ReAct paper's Reason / Act / Observe cycle. +""" from abc import ABC, abstractmethod from typing import Optional from pydantic import Field -from app.agent.base import BaseAgent +from app.agent.base import BaseAgent, Task, TaskInterrupted +from app.config import config from app.llm import LLM from app.schema import AgentState, Memory @@ -19,20 +31,72 @@ class ReActAgent(BaseAgent, ABC): memory: Memory = Field(default_factory=Memory) state: AgentState = AgentState.IDLE - max_steps: int = 10 + max_steps: int = config.agent.max_steps current_step: int = 0 @abstractmethod - async def think(self) -> bool: - """Process current state and decide next action""" + async def think(self, task: Task) -> bool: + """Process current state and decide next action. + + Must call ``task.emit('agent:lifecycle:step:reason', {...})`` with the + LLM's reasoning text before returning. + """ @abstractmethod - async def act(self) -> str: - """Execute decided actions""" + async def act(self, task: Task) -> str: + """Execute decided actions. + + Must call ``task.emit('agent:lifecycle:step:observe', {...})`` with a + summary of tool results before returning. + """ + + async def step(self, task: Task) -> str: + """Execute a single step: Reason → Act → Observe. + + Emits structured lifecycle events for the full ReAct trace: + - step:start already emitted by BaseAgent.run() + - step:reason emitted inside think() when the LLM responds + - step:act emitted here before dispatching act() + - step:observe emitted inside act() after tools complete + - step:complete emitted here with the step summary + """ + if task.is_interrupted(): + raise TaskInterrupted() + + # ── Reason ──────────────────────────────────────────────────────── + should_act = await self.think(task) + + if task.is_interrupted(): + raise TaskInterrupted() - async def step(self) -> str: - """Execute a single step: think and act.""" - should_act = await self.think() if not should_act: + task.emit( + "agent:lifecycle:step:complete", + { + "step": self.current_step, + "outcome": "no_action", + "summary": "Thinking complete — no tool action required.", + }, + ) return "Thinking complete - no action needed" - return await self.act() + + # ── Act ─────────────────────────────────────────────────────────── + task.emit( + "agent:lifecycle:step:act", + { + "step": self.current_step, + "agent": self.name, + }, + ) + observation = await self.act(task) + + # ── Observe ─────────────────────────────────────────────────────── + task.emit( + "agent:lifecycle:step:complete", + { + "step": self.current_step, + "outcome": "acted", + "summary": observation[:400] if observation else "", + }, + ) + return observation diff --git a/app/agent/sandbox_agent.py b/app/agent/sandbox_agent.py index 58612d20f..ec0ea8079 100644 --- a/app/agent/sandbox_agent.py +++ b/app/agent/sandbox_agent.py @@ -2,12 +2,12 @@ from pydantic import Field, model_validator +from app.agent.base import Task, TaskInterrupted from app.agent.browser import BrowserContextHelper from app.agent.toolcall import ToolCallAgent from app.config import config from app.daytona.sandbox import create_sandbox, delete_sandbox from app.daytona.tool_base import SandboxToolsBase -from app.logger import logger from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT from app.tool import Terminate, ToolCollection from app.tool.ask_human import AskHuman @@ -95,8 +95,6 @@ async def initialize_sandbox_tools( "vnc": vnc_url, "website": website_url, } - logger.info(f"VNC URL: {vnc_url}") - logger.info(f"Website URL: {website_url}") SandboxToolsBase._urls_printed = True sb_tools = [ SandboxBrowserTool(sandbox), @@ -106,8 +104,7 @@ async def initialize_sandbox_tools( ] self.available_tools.add_tools(*sb_tools) - except Exception as e: - logger.error(f"Error initializing sandbox tools: {e}") + except Exception: raise async def initialize_mcp_servers(self) -> None: @@ -117,9 +114,6 @@ async def initialize_mcp_servers(self) -> None: if server_config.type == "sse": if server_config.url: await self.connect_mcp_server(server_config.url, server_id) - logger.info( - f"Connected to MCP server {server_id} at {server_config.url}" - ) elif server_config.type == "stdio": if server_config.command: await self.connect_mcp_server( @@ -128,11 +122,8 @@ async def initialize_mcp_servers(self) -> None: use_stdio=True, stdio_args=server_config.args, ) - logger.info( - f"Connected to MCP server {server_id} using command {server_config.command}" - ) except Exception as e: - logger.error(f"Failed to connect to MCP server {server_id}: {e}") + _ = e async def connect_mcp_server( self, @@ -178,11 +169,9 @@ async def delete_sandbox(self, sandbox_id: str) -> None: """Delete a sandbox by ID.""" try: await delete_sandbox(sandbox_id) - logger.info(f"Sandbox {sandbox_id} deleted successfully") if sandbox_id in self.sandbox_link: del self.sandbox_link[sandbox_id] except Exception as e: - logger.error(f"Error deleting sandbox {sandbox_id}: {e}") raise e async def cleanup(self): @@ -195,8 +184,10 @@ async def cleanup(self): await self.delete_sandbox(self.sandbox.id if self.sandbox else "unknown") self._initialized = False - async def think(self) -> bool: + async def think(self, task: Task) -> bool: """Process current state and decide next actions with appropriate context.""" + if task.is_interrupted(): + raise TaskInterrupted() if not self._initialized: await self.initialize_mcp_servers() self._initialized = True @@ -215,7 +206,7 @@ async def think(self) -> bool: await self.browser_context_helper.format_next_step_prompt() ) - result = await super().think() + result = await super().think(task) # Restore original prompt self.next_step_prompt = original_prompt diff --git a/app/agent/swe.py b/app/agent/swe.py index e655a5b73..7d07778a0 100644 --- a/app/agent/swe.py +++ b/app/agent/swe.py @@ -4,7 +4,7 @@ from app.agent.toolcall import ToolCallAgent from app.prompt.swe import SYSTEM_PROMPT -from app.tool import Bash, StrReplaceEditor, Terminate, ToolCollection +from app.tool import ApplyPatchEditor, Bash, LineEdit, Terminate, ToolCollection class SWEAgent(ToolCallAgent): @@ -17,7 +17,7 @@ class SWEAgent(ToolCallAgent): next_step_prompt: str = "" available_tools: ToolCollection = ToolCollection( - Bash(), StrReplaceEditor(), Terminate() + Bash(), LineEdit(), ApplyPatchEditor(), Terminate() ) special_tool_names: List[str] = Field(default_factory=lambda: [Terminate().name]) diff --git a/app/agent/toolcall.py b/app/agent/toolcall.py index 65f31d988..92b147739 100644 --- a/app/agent/toolcall.py +++ b/app/agent/toolcall.py @@ -1,22 +1,55 @@ import asyncio +import difflib import json +import re from typing import Any, List, Optional, Union from pydantic import Field +from app.agent.base import Task, TaskInterrupted from app.agent.react import ReActAgent +from app.config import config from app.exceptions import TokenLimitExceeded -from app.logger import logger +from app.llm import MULTIMODAL_MODELS from app.prompt.toolcall import NEXT_STEP_PROMPT, SYSTEM_PROMPT from app.schema import TOOL_CHOICE_TYPE, AgentState, Message, ToolCall, ToolChoice +from app.task_context import ( + current_tool_call, + get_current_auto_context_compress, + get_current_requested_context_window, +) from app.tool import CreateChatCompletion, Terminate, ToolCollection +from app.tool.browser_use_tool import BrowserUseTool +from context.engine import ContextEngine TOOL_CALL_REQUIRED = "Tool calls required but none provided" +# Patterns that suggest the model is in the middle of executing a plan (not finished). +_INCOMPLETE_RE = re.compile( + r"\b(let me|i(?:'| a)m going to|next i(?:'| a)ll|i(?:'| a)ll now|continuing|to debug|to inspect|to verify)\b", + re.IGNORECASE, +) + +# Strong explicit finish markers — a single match is enough to consider the turn final. +_STRONG_FINAL_RE = re.compile( + r"\b(task (?:is )?(?:complete|done|finished)|all (?:steps|requirements) (?:are )?(?:done|complete|met)|here(?:'s| is) (?:the )?(?:final|complete|full)|remaining limitations|implementation (?:is )?(?:complete|done))\b", + re.IGNORECASE, +) + +# Weak finish words — need 2+ of these (or 1 + no tool intent) to avoid false positives. +_WEAK_FINAL_RE = re.compile( + r"\b(done|completed|finished|implemented|created|verified|summary|blocked|successfully)\b", + re.IGNORECASE, +) + +# Kept for external import compatibility. +FINAL_RESPONSE_RE = _STRONG_FINAL_RE + +OBSERVE_ONLY_TOOLS = {"codebase_overview", "glob", "grep", "read_files"} class ToolCallAgent(ReActAgent): - """Base agent class for handling tool/function calls with enhanced abstraction""" + """Base agent class for handling tool/function calls with enhanced abstraction.""" name: str = "toolcall" description: str = "an agent that can execute tool calls." @@ -32,36 +65,55 @@ class ToolCallAgent(ReActAgent): tool_calls: List[ToolCall] = Field(default_factory=list) _current_base64_image: Optional[str] = None + _last_assistant_content: str = "" + _consecutive_no_tool_nonfinal: int = 0 + _consecutive_observe_only_steps: int = 0 - max_steps: int = 30 + max_steps: int = config.agent.max_steps + max_tools_per_step: int = config.agent.max_tools_per_step max_observe: Optional[Union[int, bool]] = None - async def think(self) -> bool: - """Process current state and decide next actions using tools""" + async def think(self, task: Task) -> bool: + """Process current state and decide next actions using tools.""" + if task.is_interrupted(): + raise TaskInterrupted() + if self.next_step_prompt: user_msg = Message.user_message(self.next_step_prompt) self.messages += [user_msg] try: - # Get response with tool options + context = ContextEngine.build(task, agent_role=self.name) + context_msg = Message.system_message( + json.dumps(context, ensure_ascii=False) + ) + if task.is_interrupted(): + raise TaskInterrupted() + + system_msgs = ( + [Message.system_message(self.system_prompt), context_msg] + if self.system_prompt + else [context_msg] + ) + self._maybe_compress_context(task, system_msgs) + response = await self.llm.ask_tool( messages=self.messages, - system_msgs=( - [Message.system_message(self.system_prompt)] - if self.system_prompt - else None - ), + system_msgs=system_msgs, tools=self.available_tools.to_params(), tool_choice=self.tool_choices, ) except ValueError: raise except Exception as e: - # Check if this is a RetryError containing TokenLimitExceeded if hasattr(e, "__cause__") and isinstance(e.__cause__, TokenLimitExceeded): token_limit_error = e.__cause__ - logger.error( - f"🚨 Token limit error (from RetryError): {token_limit_error}" + task.emit( + "error", + { + "message": "Token limit reached during tool thinking", + "detail": str(token_limit_error), + }, ) self.memory.add_message( Message.assistant_message( @@ -75,35 +127,96 @@ async def think(self) -> bool: self.tool_calls = tool_calls = ( response.tool_calls if response and response.tool_calls else [] ) + if tool_calls and self._is_observe_only_batch(tool_calls): + self._consecutive_observe_only_steps += 1 + if self._consecutive_observe_only_steps >= 5: + task.emit( + "warning", + { + "message": "Repeated observe-only steps detected. Agent must now execute implementation/verification actions or terminate with a concrete status summary." + }, + ) + self.memory.add_message( + Message.user_message( + "Do not run more inspection-only steps now. " + "Execute the next implementation action(s), then verify. " + "If blocked, call terminate with exact blocker, completed steps, and remaining work." + ) + ) + elif tool_calls: + self._consecutive_observe_only_steps = 0 + if len(tool_calls) > self.max_tools_per_step: + tool_calls = tool_calls[: self.max_tools_per_step] + self.tool_calls = tool_calls + task.emit( + "warning", + { + "message": ( + f"Tool call batch trimmed to {self.max_tools_per_step} calls " + "to protect runtime stability." + ) + }, + ) content = response.content if response and response.content else "" + self._last_assistant_content = content.strip() + + task.emit( + "thought", + { + "agent": self.name, + "content": content, + "tool_count": len(tool_calls) if tool_calls else 0, + "tools": [call.function.name for call in tool_calls] + if tool_calls + else [], + "tool_calls": [ + { + "id": call.id, + "type": "function", + "function": { + "name": call.function.name, + "arguments": call.function.arguments, + }, + } + for call in tool_calls + ] + if tool_calls + else [], + "arguments": tool_calls[0].function.arguments if tool_calls else None, + }, + ) - # Log response info - logger.info(f"✨ {self.name}'s thoughts: {content}") - logger.info( - f"🛠️ {self.name} selected {len(tool_calls) if tool_calls else 0} tools to use" + # Structured ReAct "Reason" trace event — consumed by the UI step timeline + task.emit( + "agent:lifecycle:step:reason", + { + "step": self.current_step, + "agent": self.name, + "reasoning": content.strip() if content else "", + "will_act": bool(tool_calls), + "tools_planned": [tc.function.name for tc in tool_calls] + if tool_calls + else [], + }, ) - if tool_calls: - logger.info( - f"🧰 Tools being prepared: {[call.function.name for call in tool_calls]}" - ) - logger.info(f"🔧 Tool arguments: {tool_calls[0].function.arguments}") try: if response is None: raise RuntimeError("No response received from the LLM") - # Handle different tool_choices modes if self.tool_choices == ToolChoice.NONE: if tool_calls: - logger.warning( - f"🤔 Hmm, {self.name} tried to use tools when they weren't available!" + task.emit( + "warning", + { + "message": f"{self.name} tried to use tools when none were available" + }, ) if content: self.memory.add_message(Message.assistant_message(content)) return True return False - # Create and add assistant message assistant_msg = ( Message.from_tool_calls(content=content, tool_calls=self.tool_calls) if self.tool_calls @@ -114,13 +227,67 @@ async def think(self) -> bool: if self.tool_choices == ToolChoice.REQUIRED and not self.tool_calls: return True # Will be handled in act() - # For 'auto' mode, continue with content if no commands but content exists if self.tool_choices == ToolChoice.AUTO and not self.tool_calls: + if content.strip(): + if "[TEMPLATE_FALLBACK_FINAL]" in content: + clean_content = content.replace( + "[TEMPLATE_FALLBACK_FINAL]", "" + ).strip() + task.emit( + "final_response", + { + "message": clean_content, + "reason": "Template fallback forced graceful final summary.", + }, + ) + self.state = AgentState.FINISHED + return True + if not self._looks_final_response(content): + self._consecutive_no_tool_nonfinal += 1 + # The model produced a continuation sentence without tool calls. + # Keep the run alive and ask for an actionable next step. + task.emit( + "warning", + { + "message": "Model returned a non-final continuation without tool calls; requesting explicit completion or next action." + }, + ) + self.memory.add_message( + Message.user_message( + "Continue autonomously and follow the existing plan strictly. " + "Either call the next tool(s) now, or if all plan steps are truly done, " + "provide a final summary with what was completed, verification performed, " + "artifact/file paths, and any remaining limitations." + ) + ) + if self._consecutive_no_tool_nonfinal >= 3: + task.emit( + "warning", + { + "message": "Repeated no-tool non-final responses detected; requiring actionable next step." + }, + ) + return True + self._consecutive_no_tool_nonfinal = 0 + task.emit( + "final_response", + { + "message": content.strip(), + "reason": "Model provided a final answer without requesting another tool.", + }, + ) + self.state = AgentState.FINISHED return bool(content) return bool(self.tool_calls) except Exception as e: - logger.error(f"🚨 Oops! The {self.name}'s thinking process hit a snag: {e}") + task.emit( + "error", + { + "message": f"The {self.name}'s thinking process hit a snag", + "detail": str(e), + }, + ) self.memory.add_message( Message.assistant_message( f"Error encountered while processing: {str(e)}" @@ -128,43 +295,258 @@ async def think(self) -> bool: ) return False - async def act(self) -> str: - """Execute tool calls and handle their results""" + @staticmethod + def _looks_incomplete_response(content: str) -> bool: + text = (content or "").strip() + if not text: + return False + if "?" in text: + return True + return bool(_INCOMPLETE_RE.search(text)) + + @staticmethod + def _looks_final_response(content: str) -> bool: + """Heuristic: is this turn a genuine task completion or mid-task commentary? + + Rules (applied in order): + 1. Empty / whitespace → never final. + 2. Looks incomplete (has in-progress signal words) → not final. + 3. Strong explicit finish marker (e.g. 'task is complete') → final. + 4. Two or more weak finish words present → final. + 5. One weak finish word AND the message ends without a colon (not leading into next step) → final. + 6. Otherwise → not final. + """ + text = (content or "").strip() + if not text: + return False + if ToolCallAgent._looks_incomplete_response(text): + return False + # Rule 3: strong explicit signal is sufficient on its own + if _STRONG_FINAL_RE.search(text): + return True + weak_matches = _WEAK_FINAL_RE.findall(text) + # Rule 4: two or more weak signals + if len(weak_matches) >= 2: + return not text.endswith(":") + # Rule 5: one weak signal + not trailing into a next-step list + if len(weak_matches) == 1: + return not text.endswith(":") + return False + + @staticmethod + def _is_observe_only_batch(tool_calls: List[ToolCall]) -> bool: + names = [call.function.name for call in tool_calls] + if not names: + return False + return all(name in OBSERVE_ONLY_TOOLS for name in names) + + def _maybe_compress_context(self, task: Task, system_msgs: List[Message]) -> None: + if not get_current_auto_context_compress(): + return + requested_window = get_current_requested_context_window() + if requested_window is None or requested_window <= 0: + requested_window = self.llm.max_input_tokens + if requested_window is None or requested_window <= 0: + return + if len(self.messages) < 30: + return + try: + supports_images = self.llm.active_model in MULTIMODAL_MODELS + formatted_system = self.llm.format_messages(system_msgs, supports_images) + formatted_messages = self.llm.format_messages( + self.messages, supports_images + ) + total_tokens = self.llm.count_message_tokens( + formatted_system + formatted_messages + ) + except Exception: + return + + ratio = total_tokens / max(1, requested_window) + if ratio < 0.9: + return + + keep_recent = 24 + older = self.messages[:-keep_recent] + recent = self.messages[-keep_recent:] + if not older: + return + + lines: list[str] = [] + for msg in older[-80:]: + role = str(msg.role) + text = (msg.content or "").replace("\n", " ").strip() + if not text: + continue + if len(text) > 220: + text = text[:220] + "..." + lines.append(f"- {role}: {text}") + + summary = ( + "Compressed conversation memory to preserve context window. " + "Use this as persistent prior state:\n" + "\n".join(lines[-60:]) + ) + self.memory.messages = [Message.system_message(summary), *recent] + task.emit( + "context_compressed", + { + "before_tokens": total_tokens, + "requested_window": requested_window, + "usage_ratio": round(ratio, 4), + "kept_recent_messages": keep_recent, + "compressed_messages": max(0, len(older)), + }, + ) + + async def act(self, task: Task) -> str: + """Execute tool calls and handle their results.""" + if task.is_interrupted(): + raise TaskInterrupted() + if not self.tool_calls: if self.tool_choices == ToolChoice.REQUIRED: raise ValueError(TOOL_CALL_REQUIRED) - # Return last message content if no tool calls return self.messages[-1].content or "No content or commands to execute" results = [] - for command in self.tool_calls: - # Reset base64_image for each tool call - self._current_base64_image = None - - result = await self.execute_tool(command) + index = 0 + # Dedup set: skip tool calls with an identical name+args signature seen in this step. + seen_sigs: set[str] = set() + while index < len(self.tool_calls): + if task.is_interrupted(): + raise TaskInterrupted() + + command = self.tool_calls[index] + # --- Intra-step deduplication --- + sig = f"{command.function.name}:{command.function.arguments}" + if sig in seen_sigs: + index += 1 + task.emit( + "warning", + { + "message": f"Duplicate tool call skipped: '{command.function.name}' with identical args.", + "tool": command.function.name, + }, + ) + # Still need a tool message to keep the message chain intact + self.memory.add_message( + Message.tool_message( + content="[skipped: identical call already executed in this step]", + tool_call_id=command.id, + name=command.function.name, + ) + ) + results.append("[skipped: duplicate]") + continue + seen_sigs.add(sig) + + command = self.tool_calls[index] + if self._is_parallel_safe(command): + batch = [command] + next_index = index + 1 + while next_index < len(self.tool_calls) and self._is_parallel_safe( + self.tool_calls[next_index] + ): + batch.append(self.tool_calls[next_index]) + next_index += 1 + + batch_results = await asyncio.gather( + *(self.execute_tool(item, task) for item in batch) + ) + for item, result in zip(batch, batch_results): + if self.max_observe: + result = result[: self.max_observe] + task.emit( + "tool_result", + { + "tool": item.function.name, + "result": result, + "tool_call_id": item.id, + }, + ) + self.memory.add_message( + Message.tool_message( + content=result, + tool_call_id=item.id, + name=item.function.name, + base64_image=self._current_base64_image, + ) + ) + results.append(result) + index = next_index + continue + self._current_base64_image = None + result = await self.execute_tool(command, task) if self.max_observe: result = result[: self.max_observe] - logger.info( - f"🎯 Tool '{command.function.name}' completed its mission! Result: {result}" + task.emit( + "tool_result", + { + "tool": command.function.name, + "result": result, + "tool_call_id": command.id, + }, ) - - # Add tool response to memory - tool_msg = Message.tool_message( - content=result, - tool_call_id=command.id, - name=command.function.name, - base64_image=self._current_base64_image, + self.memory.add_message( + Message.tool_message( + content=result, + tool_call_id=command.id, + name=command.function.name, + base64_image=self._current_base64_image, + ) ) - self.memory.add_message(tool_msg) results.append(result) + index += 1 + + # ReAct "Observe" trace event — summarises what all tools returned + task.emit( + "agent:lifecycle:step:observe", + { + "step": self.current_step, + "agent": self.name, + "tool_count": len(self.tool_calls), + "tools_executed": [tc.function.name for tc in self.tool_calls], + "observation_preview": "\n\n".join(results)[:600] if results else "", + }, + ) return "\n\n".join(results) - async def execute_tool(self, command: ToolCall) -> str: - """Execute a single tool call with robust error handling""" + def _is_parallel_safe(self, command: ToolCall) -> bool: + """Return True if the tool can run concurrently with others. + + Prefers the tool instance's ``parallel_safe`` capability flag when + available; falls back to checking a hard-coded allowlist so that older + tools without the flag still batch correctly. + """ + name = (command.function.name or "").lower() + tool_instance = self.available_tools.tool_map.get(name) + if tool_instance is not None and hasattr(tool_instance, "parallel_safe"): + return bool(tool_instance.parallel_safe) + # Fallback: a conservative allowlist of known-safe tool names. + _SAFE_FALLBACK = { + "skill_playbook", + "codebase_overview", + "glob", + "grep", + "read_files", + "web_search", + } + return name in _SAFE_FALLBACK + + async def execute_tool(self, command: ToolCall, task: Task) -> str: + """Execute a single tool call with robust error handling. + + For tools that declare ``can_retry=True`` (the default), a failed first + attempt is retried once with the error injected into the arguments so + the model's implementation gets a second chance with corrective context. + """ + if task.is_interrupted(): + raise TaskInterrupted() + if not command or not command.function or not command.function.name: return "Error: Invalid command format" @@ -172,79 +554,209 @@ async def execute_tool(self, command: ToolCall) -> str: if name not in self.available_tools.tool_map: return f"Error: Unknown tool '{name}'" + tool_instance = self.available_tools.tool_map.get(name) + tool_can_retry = getattr(tool_instance, "can_retry", True) + try: - # Parse arguments args = json.loads(command.function.arguments or "{}") + except json.JSONDecodeError: + error_msg = f"Error parsing arguments for {name}: Invalid JSON format" + task.emit( + "error", + { + "message": f"Invalid JSON arguments for tool '{name}'", + "detail": command.function.arguments, + "fatal": False, + }, + ) + return f"Error: {error_msg}" - # Execute the tool - logger.info(f"🔧 Activating tool: '{name}'...") - result = await self.available_tools.execute(name=name, tool_input=args) + async def _run_once(run_args: dict) -> str: + """Execute the tool once and return the observation string.""" + token = current_tool_call.set({"id": command.id, "name": name}) + try: + result = await self.available_tools.execute( + name=name, tool_input=run_args + ) + finally: + current_tool_call.reset(token) - # Handle special tools - await self._handle_special_tool(name=name, result=result) + if name == BrowserUseTool().name: + browser_screenshot = await self._emit_browser_screenshot(task) + if browser_screenshot: + self._current_base64_image = browser_screenshot + + await self._handle_special_tool(task=task, name=name, result=result) - # Check if result is a ToolResult with base64_image if hasattr(result, "base64_image") and result.base64_image: - # Store the base64_image for later use in tool_message self._current_base64_image = result.base64_image - # Format result for display (standard case) observation = ( f"Observed output of cmd `{name}` executed:\n{str(result)}" if result else f"Cmd `{name}` completed with no output" ) + return observation, result + + try: + observation, result = await _run_once(args) + + # --- Retry with error feedback (one attempt only) --- + result_is_error = ( + isinstance(result, str) and result.lower().startswith("error") + ) or (hasattr(result, "is_error") and result.is_error) + + if result_is_error and tool_can_retry: + error_hint = str(result) + task.emit( + "warning", + { + "message": f"Tool '{name}' failed on first attempt; retrying with error context.", + "detail": error_hint, + }, + ) + retry_args = {**args, "_error_context": error_hint} + try: + observation, result = await _run_once(retry_args) + except Exception: + pass # Keep the first error observation on retry failure return observation - except json.JSONDecodeError: - error_msg = f"Error parsing arguments for {name}: Invalid JSON format" - logger.error( - f"📝 Oops! The arguments for '{name}' don't make sense - invalid JSON, arguments:{command.function.arguments}" - ) - return f"Error: {error_msg}" + except Exception as e: - error_msg = f"⚠️ Tool '{name}' encountered a problem: {str(e)}" - logger.exception(error_msg) + error_msg = f"Tool '{name}' encountered a problem: {str(e)}" + task.emit( + "error", + { + "message": "Tool execution failed", + "tool": name, + "detail": str(e), + "fatal": False, + }, + ) return f"Error: {error_msg}" - async def _handle_special_tool(self, name: str, result: Any, **kwargs): - """Handle special tool execution and state changes""" + @staticmethod + def _build_str_replace_diff_preview(args: dict) -> dict: + command = str(args.get("command") or "") + old_str = str(args.get("old_str") or "") + new_str = str(args.get("new_str") or "") + file_text = str(args.get("file_text") or "") + + def _clip( + lines: list[str], max_lines: int = 120, max_len: int = 240 + ) -> list[str]: + trimmed = [line[:max_len] for line in lines[:max_lines]] + if len(lines) > max_lines: + trimmed.append("... (diff truncated)") + return trimmed + + payload: dict[str, Any] = {"command": command, "lines": []} + + if command == "str_replace": + old_lines = old_str.splitlines() + new_lines = new_str.splitlines() + raw = list( + difflib.unified_diff( + old_lines, + new_lines, + fromfile="before", + tofile="after", + n=2, + lineterm="", + ) + ) + payload["lines"] = _clip(raw) + payload["added_lines"] = sum( + 1 for line in raw if line.startswith("+") and not line.startswith("+++") + ) + payload["deleted_lines"] = sum( + 1 for line in raw if line.startswith("-") and not line.startswith("---") + ) + return payload + + if command == "insert": + payload["lines"] = _clip([f"+{line}" for line in new_str.splitlines()]) + payload["added_lines"] = len(new_str.splitlines()) + payload["deleted_lines"] = 0 + return payload + + if command == "create": + payload["lines"] = _clip([f"+{line}" for line in file_text.splitlines()]) + payload["added_lines"] = len(file_text.splitlines()) + payload["deleted_lines"] = 0 + return payload + + payload["added_lines"] = 0 + payload["deleted_lines"] = 0 + return payload + + async def _emit_browser_screenshot(self, task: Task) -> Optional[str]: + browser_tool = self.available_tools.get_tool(BrowserUseTool().name) + if browser_tool is None or not hasattr(browser_tool, "get_current_state"): + return None + + state_result = await browser_tool.get_current_state() + screenshot = getattr(state_result, "base64_image", None) + if not screenshot: + return None + + url = "" + title = "" + try: + state = json.loads(state_result.output or "{}") + url = state.get("url", "") + title = state.get("title", "") + except Exception: + pass + + task.emit( + "browser_screenshot", + {"screenshot": screenshot, "url": url, "title": title}, + ) + return screenshot + + async def _handle_special_tool(self, task: Task, name: str, result: Any, **kwargs): + """Handle special tool execution and state changes.""" if not self._is_special_tool(name): return if self._should_finish_execution(name=name, result=result, **kwargs): - # Set agent state to finished - logger.info(f"🏁 Special tool '{name}' has completed the task!") + summary = self._last_assistant_content or str(result) + task.emit( + "finish_signal", + { + "tool": name, + "message": summary, + "reason": "Finish tool signaled completion.", + }, + ) self.state = AgentState.FINISHED @staticmethod def _should_finish_execution(**kwargs) -> bool: - """Determine if tool execution should finish the agent""" + """Determine if tool execution should finish the agent.""" return True def _is_special_tool(self, name: str) -> bool: - """Check if tool name is in special tools list""" + """Check if tool name is in special tools list.""" return name.lower() in [n.lower() for n in self.special_tool_names] async def cleanup(self): """Clean up resources used by the agent's tools.""" - logger.info(f"🧹 Cleaning up resources for agent '{self.name}'...") - for tool_name, tool_instance in self.available_tools.tool_map.items(): + for tool_instance in self.available_tools.tool_map.values(): if hasattr(tool_instance, "cleanup") and asyncio.iscoroutinefunction( tool_instance.cleanup ): try: - logger.debug(f"🧼 Cleaning up tool: {tool_name}") await tool_instance.cleanup() - except Exception as e: - logger.error( - f"🚨 Error cleaning up tool '{tool_name}': {e}", exc_info=True - ) - logger.info(f"✨ Cleanup complete for agent '{self.name}'.") + except Exception: + # Ignore cleanup errors to avoid masking main flow + pass - async def run(self, request: Optional[str] = None) -> str: + async def run(self, task: Task, input: Optional[str] = None) -> str: """Run the agent with cleanup when done.""" try: - return await super().run(request) + return await super().run(task, input) finally: await self.cleanup() diff --git a/app/config.py b/app/config.py index a881e2a5e..0c30af6bb 100644 --- a/app/config.py +++ b/app/config.py @@ -28,6 +28,14 @@ class LLMSettings(BaseModel): temperature: float = Field(1.0, description="Sampling temperature") api_type: str = Field(..., description="Azure, Openai, or Ollama") api_version: str = Field(..., description="Azure Openai version if AzureOpenai") + enable_thinking: Optional[bool] = Field( + None, + description=( + "Enable/disable thinking (reasoning) mode. " + "None = auto-detect from LM-Studio /api/v0/models; " + "true = always on; false = always off." + ), + ) class ProxySettings(BaseModel): @@ -66,6 +74,99 @@ class RunflowSettings(BaseModel): ) +class AgentSettings(BaseModel): + max_steps: int = Field(default=120, description="Maximum steps for agent execution") + max_tools_per_step: int = Field( + default=6, description="Maximum number of tool calls executed in one step" + ) + + +class RLSettings(BaseModel): + enabled: bool = Field( + default=False, + description="Enable RL policy hints for agent execution", + ) + policy_mode: str = Field( + default="base", + description="Policy mode selector: base or rl", + ) + policy_path: str = Field( + default="research/openmanus-rl/artifacts/policy/latest/policy.md", + description="Path to policy markdown relative to repository root", + ) + metadata_path: str = Field( + default="research/openmanus-rl/artifacts/policy/latest/metadata.json", + description="Path to policy metadata JSON relative to repository root", + ) + + +class AgentMemorySettings(BaseModel): + enabled: bool = Field( + default=False, + description="Enable AgentMemory integration for long-term recall.", + ) + base_url: str = Field( + default="http://localhost:3111", + description="AgentMemory REST base URL.", + ) + project: str = Field( + default="openmanus", + description="AgentMemory project namespace.", + ) + top_k: int = Field( + default=5, + description="How many memories to retrieve for contextual recall.", + ) + timeout_seconds: int = Field( + default=8, + description="HTTP timeout for AgentMemory requests.", + ) + auto_remember_completion: bool = Field( + default=True, + description="Save final task outcomes to AgentMemory.", + ) + vector_backend: str = Field( + default="none", + description='Vector backend to use: "none" or "faiss".', + ) + embedding_provider: str = Field( + default="openai_compatible", + description="Embedding provider.", + ) + embedding_model: str = Field( + default="text-embedding-nomic-embed-text-v1.5", + description="Embedding model name.", + ) + embedding_base_url: str = Field( + default="", + description="Embedding endpoint base URL (empty derives from configured LLM).", + ) + embedding_api_key: str = Field( + default="", + description="Embedding API key (empty derives from configured LLM).", + ) + vector_index_path: str = Field( + default="/app/workspace/agentmemory.faiss", + description="Path to local FAISS index file.", + ) + vector_meta_path: str = Field( + default="/app/workspace/agentmemory_vectors.json", + description="Path to vector metadata JSON file.", + ) + hybrid_search: bool = Field( + default=True, + description="Whether to combine keyword search and vector similarity search.", + ) + vector_weight: float = Field( + default=0.65, + description="Weight for vector search scores in hybrid search.", + ) + keyword_weight: float = Field( + default=0.35, + description="Weight for keyword (FTS5/BM25) search scores in hybrid search.", + ) + + class BrowserSettings(BaseModel): headless: bool = Field(False, description="Whether to run browser in headless mode") disable_security: bool = Field( @@ -103,10 +204,13 @@ class SandboxSettings(BaseModel): network_enabled: bool = Field( False, description="Whether network access is allowed" ) + docker_socket_enabled: bool = Field( + True, description="Mount host Docker socket into the sandbox" + ) class DaytonaSettings(BaseModel): - daytona_api_key: str + daytona_api_key: Optional[str] = None daytona_server_url: Optional[str] = Field( "https://app.daytona.io/api", description="" ) @@ -171,6 +275,23 @@ def load_server_config(cls) -> Dict[str, MCPServerConfig]: raise ValueError(f"Failed to load MCP server config: {e}") +class DeepSpecSettings(BaseModel): + enabled: bool = Field( + default=False, description="Enable DeepSpec research integration" + ) + repo_url: str = Field( + default="https://github.com/deepseek-ai/DeepSpec", + description="DeepSpec repository URL", + ) + checkout_dir: str = Field( + default="research/deepspec", description="Checkout directory for DeepSpec" + ) + mode: str = Field(default="research", description="Mode of operation") + target_model: str = Field( + default="Qwen/Qwen3-4B", description="Target model for DeepSpec" + ) + + class AppConfig(BaseModel): llm: Dict[str, LLMSettings] sandbox: Optional[SandboxSettings] = Field( @@ -189,6 +310,20 @@ class AppConfig(BaseModel): daytona_config: Optional[DaytonaSettings] = Field( None, description="Daytona configuration" ) + agent: Optional[AgentSettings] = Field( + default_factory=AgentSettings, description="Agent execution settings" + ) + rl: Optional[RLSettings] = Field( + default_factory=RLSettings, description="RL integration settings" + ) + agentmemory: Optional[AgentMemorySettings] = Field( + default_factory=AgentMemorySettings, + description="AgentMemory integration settings", + ) + deepspec: Optional[DeepSpecSettings] = Field( + default_factory=DeepSpecSettings, + description="DeepSpec research integration settings", + ) class Config: arbitrary_types_allowed = True @@ -246,6 +381,7 @@ def _load_initial_config(self): "temperature": base_llm.get("temperature", 1.0), "api_type": base_llm.get("api_type", ""), "api_version": base_llm.get("api_version", ""), + "enable_thinking": base_llm.get("enable_thinking"), # None = auto-detect } # handle browser config. @@ -324,6 +460,10 @@ def _load_initial_config(self): "mcp_config": mcp_settings, "run_flow_config": run_flow_settings, "daytona_config": daytona_settings, + "agent": AgentSettings(**raw_config.get("agent", {})), + "rl": RLSettings(**raw_config.get("rl", {})), + "agentmemory": AgentMemorySettings(**raw_config.get("agentmemory", {})), + "deepspec": DeepSpecSettings(**raw_config.get("deepspec", {})), } self._config = AppConfig(**config_dict) @@ -358,11 +498,31 @@ def run_flow_config(self) -> RunflowSettings: """Get the Run Flow configuration""" return self._config.run_flow_config + @property + def agent(self) -> AgentSettings: + """Get the Agent configuration""" + return self._config.agent + + @property + def rl(self) -> RLSettings: + """Get the RL integration configuration""" + return self._config.rl + @property def workspace_root(self) -> Path: """Get the workspace root directory""" return WORKSPACE_ROOT + @property + def agentmemory(self) -> AgentMemorySettings: + """Get the AgentMemory integration configuration.""" + return self._config.agentmemory + + @property + def deepspec(self) -> DeepSpecSettings: + """Get the DeepSpec research integration configuration.""" + return self._config.deepspec + @property def root_path(self) -> Path: """Get the root path of the application""" diff --git a/app/daytona/sandbox.py b/app/daytona/sandbox.py index 8970b9cef..d195ae931 100644 --- a/app/daytona/sandbox.py +++ b/app/daytona/sandbox.py @@ -38,8 +38,15 @@ else: logger.warning("No Daytona target found in environment variables") -daytona = Daytona(daytona_config) -logger.info("Daytona client initialized") +daytona = None +if daytona_config.api_key and daytona_config.api_key != "placeholder": + try: + daytona = Daytona(daytona_config) + logger.info("Daytona client initialized") + except Exception as e: + logger.error(f"Failed to initialize Daytona client: {e}") +else: + logger.info("Daytona client not initialized (no valid API key)") async def get_or_start_sandbox(sandbox_id: str): diff --git a/app/daytona/tool_base.py b/app/daytona/tool_base.py index 043578a9a..1de1d6d58 100644 --- a/app/daytona/tool_base.py +++ b/app/daytona/tool_base.py @@ -3,7 +3,7 @@ from typing import Any, ClassVar, Dict, Optional from daytona import Daytona, DaytonaConfig, Sandbox, SandboxState -from pydantic import Field +from pydantic import ConfigDict, Field from app.config import config from app.daytona.sandbox import create_sandbox, start_supervisord_session @@ -19,7 +19,15 @@ server_url=daytona_settings.daytona_server_url, target=daytona_settings.daytona_target, ) -daytona = Daytona(daytona_config) +daytona = None +if daytona_config.api_key and daytona_config.api_key != "placeholder": + try: + daytona = Daytona(daytona_config) + logger.info("Daytona client initialized") + except Exception as e: + logger.error(f"Failed to initialize Daytona client: {e}") +else: + logger.info("Daytona client not initialized (no valid API key)") @dataclass @@ -64,9 +72,7 @@ class SandboxToolsBase(BaseTool): workspace_path: str = Field(default="/workspace", exclude=True) _sessions: dict[str, str] = {} - class Config: - arbitrary_types_allowed = True # Allow non-pydantic types like ThreadManager - underscore_attrs_are_private = True + model_config = ConfigDict(arbitrary_types_allowed=True) async def _ensure_sandbox(self) -> Sandbox: """Ensure we have a valid sandbox instance, retrieving it from the project if needed.""" diff --git a/app/llm.py b/app/llm.py index 82ebe8857..412861669 100644 --- a/app/llm.py +++ b/app/llm.py @@ -13,7 +13,7 @@ from openai.types.chat import ChatCompletion, ChatCompletionMessage from tenacity import ( retry, - retry_if_exception_type, + retry_if_exception, stop_after_attempt, wait_random_exponential, ) @@ -29,9 +29,28 @@ Message, ToolChoice, ) +from app.task_context import ( + emit_current_task, + get_current_llm_connection, + get_current_model, +) + + +# Models that use max_completion_tokens instead of max_tokens (cloud providers). +# For local models (LM-Studio / Ollama) these are detected dynamically. +REASONING_MODELS = ["o1", "o1-mini", "o3", "o3-mini", "o4-mini"] + +# Models that require reasoning_effort instead of temperature (OpenAI cloud only) +REASONING_EFFORT_MODELS = {"o3", "o3-mini", "o4-mini"} +# Claude models with extended thinking support (cloud only — local models use auto-detect) +CLAUDE_THINKING_MODELS = { + "claude-3-7-sonnet-20250219", + "claude-3-5-sonnet-20241022", + "claude-3-opus-20240229", +} -REASONING_MODELS = ["o1", "o3-mini"] +# Cloud multimodal models (local models report vision capability via /api/v0/models) MULTIMODAL_MODELS = [ "gpt-4-vision-preview", "gpt-4o", @@ -39,8 +58,45 @@ "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307", + "claude-3-5-sonnet-20241022", + "claude-3-5-haiku-20241022", + "claude-3-7-sonnet-20250219", + "gemini-1.5-pro", + "gemini-1.5-flash", + "gemini-2.0-flash", ] +# --- Local-server detection helpers --- + +_LOCAL_HOSTS = {"localhost", "127.0.0.1", "0.0.0.0", "::1"} + + +def _is_local_server(base_url: str) -> bool: + """Return True when base_url points at a local inference server.""" + try: + from urllib.parse import urlparse + + host = urlparse(base_url).hostname or "" + return ( + host in _LOCAL_HOSTS + or host.startswith("192.168.") + or host.startswith("10.") + ) + except Exception: + return False + + +def _should_retry_llm_exception(exc: Exception) -> bool: + """Retry transient provider failures only.""" + if isinstance(exc, (TokenLimitExceeded, ValueError)): + return False + text = str(exc).lower() + if "no user query found in messages" in text: + return False + if "jinja template" in text and "prompt template" in text: + return False + return isinstance(exc, (RateLimitError, APIError, OpenAIError)) + class TokenCounter: # Token constants @@ -171,60 +227,307 @@ def count_message_tokens(self, messages: List[dict]) -> int: return total_tokens +# --------------------------------------------------------------------------- +# LLM instance registry +# --------------------------------------------------------------------------- +# Keyed by config_name. Populated by get_llm(); LLM() also populates it. +_llm_registry: Dict[str, "LLM"] = {} + + +def get_llm( + config_name: str = "default", llm_config: Optional["LLMSettings"] = None +) -> "LLM": + """Return a cached LLM instance for *config_name*. + + This is the preferred factory. ``LLM(config_name)`` is kept for + backwards-compatibility and delegates here. + + Why a factory instead of __new__? + - ``__new__`` + ``__init__`` is fragile: Python calls ``__init__`` again + every time even when ``__new__`` returns an existing instance, requiring + the ``if not hasattr(self, 'client')`` guard. + - A module-level dict + factory gives the same per-config caching without + the hidden re-init risk, and makes the caching explicit and testable. + - Callers that need a fresh instance (e.g. tests) can call + ``get_llm.cache_clear(config_name)`` without monkey-patching ``__new__``. + """ + if config_name not in _llm_registry: + instance = object.__new__(LLM) + instance._init_from_config(config_name, llm_config) + _llm_registry[config_name] = instance + return _llm_registry[config_name] + + +def _evict_llm(config_name: str = "default") -> None: + """Remove a cached LLM instance so the next call to get_llm() re-creates it. + + Useful in tests or when the config changes at runtime. + """ + _llm_registry.pop(config_name, None) + + class LLM: - _instances: Dict[str, "LLM"] = {} + """Thin wrapper around an OpenAI-compatible async client with capability detection.""" + + def __init__( + self, config_name: str = "default", llm_config: Optional[LLMSettings] = None + ): + """Backwards-compatible constructor — delegates to get_llm() cache. + + Calling ``LLM(config_name)`` is equivalent to ``get_llm(config_name)``. + Because Python always calls ``__init__`` after ``__new__``, we guard + against double-initialisation with the ``_init_from_config`` split. + """ + # If this instance was already initialised (retrieved from cache or + # created by get_llm()), do nothing. + if hasattr(self, "client"): + return + self._init_from_config(config_name, llm_config) def __new__( cls, config_name: str = "default", llm_config: Optional[LLMSettings] = None ): - if config_name not in cls._instances: - instance = super().__new__(cls) - instance.__init__(config_name, llm_config) - cls._instances[config_name] = instance - return cls._instances[config_name] - - def __init__( + """Return the cached instance from the module-level registry.""" + if config_name not in _llm_registry: + instance = object.__new__(cls) + # _init_from_config will be called by __init__ below + _llm_registry[config_name] = instance + return _llm_registry[config_name] + + def _init_from_config( self, config_name: str = "default", llm_config: Optional[LLMSettings] = None - ): - if not hasattr(self, "client"): # Only initialize if not already initialized - llm_config = llm_config or config.llm - llm_config = llm_config.get(config_name, llm_config["default"]) - self.model = llm_config.model - self.max_tokens = llm_config.max_tokens - self.temperature = llm_config.temperature - self.api_type = llm_config.api_type - self.api_key = llm_config.api_key - self.api_version = llm_config.api_version - self.base_url = llm_config.base_url - - # Add token counting related attributes - self.total_input_tokens = 0 - self.total_completion_tokens = 0 - self.max_input_tokens = ( - llm_config.max_input_tokens - if hasattr(llm_config, "max_input_tokens") - else None + ) -> None: + """Initialise the instance from config. Called at most once per instance.""" + if hasattr(self, "client"): # Already initialised — skip. + return + + llm_config = llm_config or config.llm + llm_config = llm_config.get(config_name, llm_config["default"]) + self.model = llm_config.model + self.max_tokens = llm_config.max_tokens + self.temperature = llm_config.temperature + self.api_type = llm_config.api_type + self.api_key = llm_config.api_key + self.api_version = llm_config.api_version + self.base_url = llm_config.base_url + + # Add token counting related attributes + self.total_input_tokens = 0 + self.total_completion_tokens = 0 + self.max_input_tokens = ( + llm_config.max_input_tokens + if hasattr(llm_config, "max_input_tokens") + else None + ) + + # Initialize tokenizer + try: + self.tokenizer = tiktoken.encoding_for_model(self.model) + except KeyError: + # If the model is not in tiktoken's presets, use cl100k_base as default + self.tokenizer = tiktoken.get_encoding("cl100k_base") + + if self.api_type == "azure": + self.client = AsyncAzureOpenAI( + base_url=self.base_url, + api_key=self.api_key, + api_version=self.api_version, ) + elif self.api_type == "aws": + self.client = BedrockClient() + else: + self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url) + + self.token_counter = TokenCounter(self.tokenizer) + + # --- Capability flags --- + # These control thinking/vision behaviour and are used in ask_tool(). + # For cloud models they start from the static name-list defaults; + # for local servers (LM-Studio / Ollama) we try to probe the API. + self._enable_thinking: Optional[bool] = getattr( + llm_config, "enable_thinking", None + ) + self.caps_thinking: bool = self.model in CLAUDE_THINKING_MODELS + self.caps_vision: bool = self.model in MULTIMODAL_MODELS + + if _is_local_server(self.base_url): + self._probe_local_server_caps() + + # ------------------------------------------------------------------ + # Local-server capability probe + # ------------------------------------------------------------------ - # Initialize tokenizer - try: - self.tokenizer = tiktoken.encoding_for_model(self.model) - except KeyError: - # If the model is not in tiktoken's presets, use cl100k_base as default - self.tokenizer = tiktoken.get_encoding("cl100k_base") - - if self.api_type == "azure": - self.client = AsyncAzureOpenAI( - base_url=self.base_url, - api_key=self.api_key, - api_version=self.api_version, + def _probe_local_server_caps(self) -> None: + """Synchronously probe LM-Studio (or Ollama) for the active model's + capability flags (thinking / vision). + + LM-Studio exposes /api/v0/models with per-model ``info`` objects: + { "id": "...", "info": { "vision": bool, "reasoning": bool } } + + The ``reasoning`` flag is True for models with built-in chain-of-thought + (QwQ, DeepSeek-R1, Phi-4 reasoning, Gemma 3 thinking variants, etc.). + + The user can always override via ``enable_thinking`` in config.toml. + """ + import json as _json + import urllib.error + import urllib.request + from urllib.parse import urlparse, urlunparse + + parsed = urlparse(self.base_url) + origin = urlunparse((parsed.scheme, parsed.netloc, "", "", "", "")) + + # --- Try LM-Studio first --- + lms_url = f"{origin}/api/v0/models" + try: + with urllib.request.urlopen(lms_url, timeout=3) as resp: + data = _json.loads(resp.read()) + models: list = data.get("data", []) + # Find the entry matching the configured model id + match = next( + ( + m + for m in models + if m.get("id") == self.model or self.model in str(m.get("id", "")) + ), + None, + ) + if match: + info = match.get("info", {}) + detected_thinking = bool(info.get("reasoning", False)) + detected_vision = bool(info.get("vision", False)) + self.caps_thinking = detected_thinking + self.caps_vision = detected_vision + logger.info( + "[LM-Studio] Model '%s' caps → thinking=%s vision=%s", + self.model, + self.caps_thinking, + self.caps_vision, ) - elif self.api_type == "aws": - self.client = BedrockClient() else: - self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url) + logger.debug( + "[LM-Studio] Could not find model '%s' in /api/v0/models list; keeping defaults.", + self.model, + ) + return # LM-Studio responded — skip Ollama probe + except Exception as exc: + logger.debug( + "[LM-Studio] /api/v0/models probe failed: %s — trying Ollama.", exc + ) + + # --- Fallback: Ollama /api/tags --- + ollama_url = f"{origin}/api/tags" + try: + with urllib.request.urlopen(ollama_url, timeout=3) as resp: + data = _json.loads(resp.read()) + model_names = [m.get("name", "") for m in data.get("models", [])] + if any(self.model in name for name in model_names): + logger.debug( + "[Ollama] Model '%s' found; capability auto-detect not available " + "— use enable_thinking in config.toml to override.", + self.model, + ) + except Exception as exc: + logger.debug("[Ollama] /api/tags probe failed: %s", exc) - self.token_counter = TokenCounter(self.tokenizer) + @property + def thinking_enabled(self) -> bool: + """True when thinking/reasoning mode should be activated for this model. + + Resolution order (highest priority first): + 1. ``enable_thinking`` in config.toml (explicit user override) + 2. Capability flag from local-server probe (LM-Studio ``reasoning`` flag) + 3. Cloud model name in ``CLAUDE_THINKING_MODELS`` + """ + if self._enable_thinking is not None: + return bool(self._enable_thinking) + return self.caps_thinking + + @property + def vision_enabled(self) -> bool: + """True when the active model supports image inputs.""" + return self.caps_vision + + # ------------------------------------------------------------------ + + @property + def active_model(self) -> str: + connection = get_current_llm_connection() or {} + return get_current_model() or connection.get("model") or self.model + + def active_request_overrides(self) -> dict: + connection = get_current_llm_connection() or {} + return { + key: value + for key, value in { + "base_url": connection.get("base_url"), + "api_key": connection.get("api_key"), + "api_type": connection.get("api_type"), + "max_tokens": connection.get("max_tokens"), + "temperature": connection.get("temperature"), + }.items() + if value not in (None, "") and not self._is_masked_value(value) + } + + @staticmethod + def _is_masked_value(value: object) -> bool: + return isinstance(value, str) and value.strip() == "********" + + @staticmethod + def _safe_int(value: object, fallback: int) -> int: + if LLM._is_masked_value(value): + return fallback + try: + return int(value) # type: ignore[arg-type] + except (TypeError, ValueError): + return fallback + + @staticmethod + def _safe_float(value: object, fallback: float) -> float: + if LLM._is_masked_value(value): + return fallback + try: + return float(value) # type: ignore[arg-type] + except (TypeError, ValueError): + return fallback + + def active_client(self): + overrides = self.active_request_overrides() + api_type = overrides.get("api_type", self.api_type) + if not overrides: + return self.client + if api_type == "aws": + return BedrockClient() + if api_type == "azure": + return AsyncAzureOpenAI( + base_url=overrides.get("base_url", self.base_url), + api_key=overrides.get("api_key", self.api_key), + api_version=self.api_version, + ) + return AsyncOpenAI( + api_key=overrides.get("api_key", self.api_key), + base_url=overrides.get("base_url", self.base_url), + ) + + def active_max_tokens(self) -> int: + overrides = self.active_request_overrides() + return self._safe_int( + overrides.get("max_tokens", self.max_tokens), self.max_tokens + ) + + def active_temperature(self) -> float: + overrides = self.active_request_overrides() + return self._safe_float( + overrides.get("temperature", self.temperature), self.temperature + ) + + def active_base_url(self) -> str: + overrides = self.active_request_overrides() + return str(overrides.get("base_url", self.base_url) or "") + + def active_api_type(self) -> str: + overrides = self.active_request_overrides() + return str(overrides.get("api_type", self.api_type) or "") def count_tokens(self, text: str) -> int: """Calculate the number of tokens in a text""" @@ -240,6 +543,15 @@ def update_token_count(self, input_tokens: int, completion_tokens: int = 0) -> N # Only track tokens if max_input_tokens is set self.total_input_tokens += input_tokens self.total_completion_tokens += completion_tokens + emit_current_task( + "token_count", + { + "input": input_tokens, + "completion": completion_tokens, + "total_input": self.total_input_tokens, + "total_completion": self.total_completion_tokens, + }, + ) logger.info( f"Token usage: Input={input_tokens}, Completion={completion_tokens}, " f"Cumulative Input={self.total_input_tokens}, Cumulative Completion={self.total_completion_tokens}, " @@ -351,12 +663,112 @@ def format_messages( return formatted_messages + @staticmethod + def ensure_user_query(messages: List[dict]) -> List[dict]: + """Ensure local chat templates always see a user query. + + Some OpenAI-compatible local servers, notably LM Studio templates for + tool-capable models, reject requests that contain only system/tool + context or end with a tool observation. Keep the transcript intact, but + add a small user continuation when needed so those templates have a + concrete query to render. + + Note: this method NEVER mutates the input list; it always returns a new list. + """ + if any(message.get("role") == "user" for message in messages): + if messages and messages[-1].get("role") in {"tool", "assistant", "system"}: + # Non-mutating: return a new list + return [ + *messages, + { + "role": "user", + "content": ( + "Continue from the latest observation. If the task is complete, " + "provide the final answer or call the finish tool with a summary." + ), + }, + ] + return messages + + for index in range(len(messages) - 1, -1, -1): + if messages[index].get("role") == "system": + # Return a new list with the system message role swapped to user + return [ + *messages[:index], + {**messages[index], "role": "user"}, + *messages[index + 1 :], + ] + + return [*messages, {"role": "user", "content": "Continue."}] + + def needs_local_template_compat(self) -> bool: + """Return true for OpenAI-compatible local servers with brittle chat templates.""" + base_url = self.active_base_url().lower() + api_type = self.active_api_type().lower() + return ( + api_type in {"ollama", "lmstudio", "local"} + or ":1234" in base_url + or "localhost" in base_url + or "127.0.0.1" in base_url + or "lmstudio" in base_url + ) + + @staticmethod + def flatten_tool_history_for_templates(messages: List[dict]) -> List[dict]: + """Convert OpenAI tool transcript details into plain chat messages. + + Some local model templates throw "No user query found" when `tool` + roles or assistant `tool_calls` appear deep in the history. The model + still needs the observations, so keep them as compact user-visible text. + """ + flattened: list[dict] = [] + for message in messages: + role = message.get("role") + content = message.get("content") or "" + + if role == "tool": + name = message.get("name") or message.get("tool_call_id") or "tool" + flattened.append( + { + "role": "user", + "content": f"Tool observation from {name}:\n{content}", + } + ) + continue + + if role == "assistant" and message.get("tool_calls"): + if content: + flattened.append({"role": "assistant", "content": content}) + calls = [] + for call in message.get("tool_calls") or []: + function = call.get("function") or {} + calls.append( + f"- {function.get('name', 'tool')}({function.get('arguments', '{}')})" + ) + if calls: + flattened.append( + { + "role": "user", + "content": "Assistant requested these tools:\n" + + "\n".join(calls), + } + ) + continue + + cleaned = { + key: value + for key, value in message.items() + if key not in {"tool_calls", "tool_call_id", "name"} + } + if cleaned.get("content") or cleaned.get("role") == "system": + flattened.append(cleaned) + + return flattened + @retry( wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6), - retry=retry_if_exception_type( - (OpenAIError, Exception, ValueError) - ), # Don't retry TokenLimitExceeded + retry=retry_if_exception(_should_retry_llm_exception), ) async def ask( self, @@ -385,7 +797,8 @@ async def ask( """ try: # Check if the model supports images - supports_images = self.model in MULTIMODAL_MODELS + model = self.active_model + supports_images = model in MULTIMODAL_MODELS # Format system and user messages with image support check if system_msgs: @@ -404,21 +817,23 @@ async def ask( raise TokenLimitExceeded(error_message) params = { - "model": self.model, + "model": model, "messages": messages, } - if self.model in REASONING_MODELS: - params["max_completion_tokens"] = self.max_tokens + if model in REASONING_MODELS: + params["max_completion_tokens"] = self.active_max_tokens() else: - params["max_tokens"] = self.max_tokens + params["max_tokens"] = self.active_max_tokens() params["temperature"] = ( - temperature if temperature is not None else self.temperature + temperature + if temperature is not None + else self.active_temperature() ) if not stream: # Non-streaming request - response = await self.client.chat.completions.create( + response = await self.active_client().chat.completions.create( **params, stream=False ) @@ -432,30 +847,36 @@ async def ask( return response.choices[0].message.content - # Streaming request, For streaming, update estimated token count before making the request + # Streaming: estimate input tokens upfront; real usage arrives in the final chunk self.update_token_count(input_tokens) - response = await self.client.chat.completions.create(**params, stream=True) + response = await self.active_client().chat.completions.create( + **params, stream=True, stream_options={"include_usage": True} + ) collected_messages = [] - completion_text = "" + real_completion_tokens: int = 0 async for chunk in response: - chunk_message = chunk.choices[0].delta.content or "" - collected_messages.append(chunk_message) - completion_text += chunk_message - print(chunk_message, end="", flush=True) + # The final chunk carries usage data (via stream_options); others carry content. + if chunk.usage: + real_completion_tokens = chunk.usage.completion_tokens or 0 + if chunk.choices: + chunk_message = chunk.choices[0].delta.content or "" + collected_messages.append(chunk_message) - print() # Newline after streaming full_response = "".join(collected_messages).strip() if not full_response: raise ValueError("Empty response from streaming LLM") - # estimate completion tokens for streaming response - completion_tokens = self.count_tokens(completion_text) + # Use real token count when available; fall back to estimation. + completion_tokens = real_completion_tokens or self.count_tokens( + full_response + ) logger.info( - f"Estimated completion tokens for streaming response: {completion_tokens}" + f"Streaming completion tokens: {completion_tokens} " + f"({'real' if real_completion_tokens else 'estimated'})" ) - self.total_completion_tokens += completion_tokens + self.update_token_count(0, completion_tokens) return full_response @@ -481,9 +902,7 @@ async def ask( @retry( wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6), - retry=retry_if_exception_type( - (OpenAIError, Exception, ValueError) - ), # Don't retry TokenLimitExceeded + retry=retry_if_exception(_should_retry_llm_exception), ) async def ask_with_images( self, @@ -515,9 +934,10 @@ async def ask_with_images( try: # For ask_with_images, we always set supports_images to True because # this method should only be called with models that support images - if self.model not in MULTIMODAL_MODELS: + model = self.active_model + if model not in MULTIMODAL_MODELS: raise ValueError( - f"Model {self.model} does not support images. Use a model from {MULTIMODAL_MODELS}" + f"Model {model} does not support images. Use a model from {MULTIMODAL_MODELS}" ) # Format messages with image support @@ -574,33 +994,37 @@ async def ask_with_images( # Set up API parameters params = { - "model": self.model, + "model": model, "messages": all_messages, "stream": stream, } # Add model-specific parameters - if self.model in REASONING_MODELS: - params["max_completion_tokens"] = self.max_tokens + if model in REASONING_MODELS: + params["max_completion_tokens"] = self.active_max_tokens() else: - params["max_tokens"] = self.max_tokens + params["max_tokens"] = self.active_max_tokens() params["temperature"] = ( - temperature if temperature is not None else self.temperature + temperature + if temperature is not None + else self.active_temperature() ) # Handle non-streaming request if not stream: - response = await self.client.chat.completions.create(**params) + response = await self.active_client().chat.completions.create(**params) if not response.choices or not response.choices[0].message.content: raise ValueError("Empty or invalid response from LLM") - self.update_token_count(response.usage.prompt_tokens) + self.update_token_count( + response.usage.prompt_tokens, response.usage.completion_tokens + ) return response.choices[0].message.content # Handle streaming request self.update_token_count(input_tokens) - response = await self.client.chat.completions.create(**params) + response = await self.active_client().chat.completions.create(**params) collected_messages = [] async for chunk in response: @@ -637,9 +1061,7 @@ async def ask_with_images( @retry( wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6), - retry=retry_if_exception_type( - (OpenAIError, Exception, ValueError) - ), # Don't retry TokenLimitExceeded + retry=retry_if_exception(_should_retry_llm_exception), ) async def ask_tool( self, @@ -672,13 +1094,46 @@ async def ask_tool( OpenAIError: If API call fails after retries Exception: For unexpected errors """ + + def _template_error_text(exc: Exception) -> str: + return str(exc).lower() + + def _is_template_user_query_error(exc: Exception) -> bool: + text = _template_error_text(exc) + return "no user query found in messages" in text or ( + "jinja template" in text and "prompt template" in text + ) + + def _build_template_fallback_messages( + source_messages: List[dict], + ) -> List[dict]: + # Preserve intent while avoiding brittle tool/template transcript shapes. + snippets: list[str] = [] + for msg in reversed(source_messages): + role = str(msg.get("role") or "") + content = str(msg.get("content") or "").strip() + if role in {"assistant", "tool", "user"} and content: + snippets.append(f"{role}: {content[:500]}") + if len(snippets) >= 8: + break + snippets.reverse() + fallback_prompt = ( + "The local model template rejected the full tool transcript. " + "Continue the task from this compact context. " + "Return a FINAL SUMMARY now with: completed work, verification, " + "artifacts/paths, and remaining limitations. Do not request more tools.\n\n" + + ("\n".join(snippets) if snippets else "No prior context available.") + ) + return [{"role": "user", "content": fallback_prompt}] + try: # Validate tool_choice if tool_choice not in TOOL_CHOICE_VALUES: raise ValueError(f"Invalid tool_choice: {tool_choice}") # Check if the model supports images - supports_images = self.model in MULTIMODAL_MODELS + model = self.active_model + supports_images = model in MULTIMODAL_MODELS # Format messages if system_msgs: @@ -687,6 +1142,11 @@ async def ask_tool( else: messages = self.format_messages(messages, supports_images) + messages = self.ensure_user_query(messages) + if self.needs_local_template_compat(): + messages = self.flatten_tool_history_for_templates(messages) + messages = self.ensure_user_query(messages) + # Calculate input token count input_tokens = self.count_message_tokens(messages) @@ -710,9 +1170,13 @@ async def ask_tool( if not isinstance(tool, dict) or "type" not in tool: raise ValueError("Each tool must be a dict with 'type' field") + # Pop thinking_budget from kwargs before building params (Claude extended thinking) + thinking_budget: Optional[int] = kwargs.pop("thinking_budget", None) + reasoning_effort: Optional[str] = kwargs.pop("reasoning_effort", None) + # Set up the completion request params = { - "model": self.model, + "model": model, "messages": messages, "tools": tools, "tool_choice": tool_choice, @@ -720,23 +1184,70 @@ async def ask_tool( **kwargs, } - if self.model in REASONING_MODELS: - params["max_completion_tokens"] = self.max_tokens + if model in REASONING_MODELS: + params["max_completion_tokens"] = self.active_max_tokens() + # o3 / o4-mini accept reasoning_effort instead of temperature + if model in REASONING_EFFORT_MODELS: + effort = reasoning_effort or "medium" + params["reasoning_effort"] = effort + # temperature is not valid for these models + params.pop("temperature", None) else: - params["max_tokens"] = self.max_tokens + params["max_tokens"] = self.active_max_tokens() params["temperature"] = ( - temperature if temperature is not None else self.temperature + temperature + if temperature is not None + else self.active_temperature() ) + # Claude extended thinking / LM-Studio reasoning mode: + # Prefer the instance-level thinking_enabled property which respects + # the user's enable_thinking config and LM-Studio auto-detection. + effective_thinking = self.thinking_enabled + if thinking_budget and not effective_thinking: + # Caller explicitly passed a budget — treat as opt-in override + effective_thinking = True + + if effective_thinking: + # Cloud Claude: structured "thinking" block + if model in CLAUDE_THINKING_MODELS and thinking_budget: + params["thinking"] = { + "type": "enabled", + "budget_tokens": int(thinking_budget), + } + logger.debug( + "Claude extended thinking enabled: budget=%d tokens, model=%s", + thinking_budget, + model, + ) + # LM-Studio / local reasoning models: pass thinking_budget as + # extra_body so the server can honour it if supported. + elif _is_local_server(self.base_url) and thinking_budget: + params.setdefault("extra_body", {})["thinking"] = { + "type": "enabled", + "budget_tokens": int(thinking_budget), + } + logger.debug( + "Local-model thinking enabled: budget=%d tokens, model=%s", + thinking_budget, + model, + ) + elif effective_thinking: + logger.debug( + "Thinking mode active for model '%s' but no budget provided — skipping block.", + model, + ) + params["stream"] = False # Always use non-streaming for tool requests - response: ChatCompletion = await self.client.chat.completions.create( - **params + response: ChatCompletion = ( + await self.active_client().chat.completions.create(**params) ) # Check if response is valid if not response.choices or not response.choices[0].message: - print(response) - # raise ValueError("Invalid or empty response from LLM") + logger.warning( + "LLM returned an empty or invalid response: %s", response + ) return None # Update token counts @@ -760,6 +1271,65 @@ async def ask_tool( logger.error("Rate limit exceeded. Consider increasing retry attempts.") elif isinstance(oe, APIError): logger.error(f"API error: {oe}") + if _is_template_user_query_error(oe): + # One-shot fallback for brittle local templates: + # retry with a compact plain user message and no tool schema. + try: + emit_current_task( + "warning", + { + "message": "Model template rejected tool transcript; using compact fallback prompt.", + "detail": str(oe), + "fatal": False, + }, + ) + fallback_messages = _build_template_fallback_messages(messages) + fallback_params = { + "model": model, + "messages": fallback_messages, + "timeout": timeout, + "stream": False, + } + if model in REASONING_MODELS: + fallback_params[ + "max_completion_tokens" + ] = self.active_max_tokens() + else: + fallback_params["max_tokens"] = self.active_max_tokens() + fallback_params["temperature"] = ( + temperature + if temperature is not None + else self.active_temperature() + ) + fallback_response: ChatCompletion = ( + await self.active_client().chat.completions.create( + **fallback_params + ) + ) + if ( + fallback_response.choices + and fallback_response.choices[0].message is not None + ): + usage = fallback_response.usage + if usage is not None: + self.update_token_count( + usage.prompt_tokens, usage.completion_tokens + ) + # Mark fallback summaries so the agent can terminate gracefully. + fb_msg = fallback_response.choices[0].message + if fb_msg.content: + fb_msg.content = ( + "[TEMPLATE_FALLBACK_FINAL]\n" + fb_msg.content + ) + return fb_msg + except Exception as fallback_error: + logger.error( + f"Fallback after template error failed: {fallback_error}" + ) + raise ValueError( + "Model prompt template rejected this tool-call transcript " + "(No user query found). Use a tool-capable template/model." + ) from oe raise except Exception as e: logger.error(f"Unexpected error in ask_tool: {e}") diff --git a/app/memory/__init__.py b/app/memory/__init__.py new file mode 100644 index 000000000..65c1203b3 --- /dev/null +++ b/app/memory/__init__.py @@ -0,0 +1,4 @@ +from app.memory.agentmemory import AgentMemoryClient, MemoryHit, agentmemory + + +__all__ = ["AgentMemoryClient", "MemoryHit", "agentmemory"] diff --git a/app/memory/agentmemory.py b/app/memory/agentmemory.py new file mode 100644 index 000000000..21b44ba74 --- /dev/null +++ b/app/memory/agentmemory.py @@ -0,0 +1,574 @@ +from __future__ import annotations + +import json +import os +import sqlite3 +import threading +import urllib.request +from dataclasses import dataclass +from typing import Any, Optional + +from app.config import config +from app.utils.logger import logger + + +@dataclass +class MemoryHit: + title: str + content: str + score: float + memory_id: Optional[int] = None + + +class AgentMemoryClient: + """Robust local SQLite + FAISS AgentMemory implementation.""" + + def __init__(self) -> None: + self.settings = config.agentmemory + self.enabled_in_config = self.settings.enabled + self.project = self.settings.project + self.db_path = "/app/workspace/agentmemory.db" + self._vector_lock = threading.Lock() + self.last_vector_error: Optional[str] = None + self._init_db() + + @property + def enabled(self) -> bool: + return bool(self.enabled_in_config) + + def _get_conn(self) -> sqlite3.Connection: + os.makedirs(os.path.dirname(self.db_path), exist_ok=True) + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + return conn + + def _init_db(self) -> None: + if not self.enabled: + return + try: + with self._get_conn() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS memories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + conversation_id TEXT NOT NULL, + project TEXT NOT NULL, + title TEXT NOT NULL, + content TEXT NOT NULL, + concepts TEXT, + metadata TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """ + ) + try: + conn.execute( + """ + CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5( + title, content, tokenize="unicode61" + ) + """ + ) + conn.execute( + """ + CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN + INSERT INTO memories_fts(rowid, title, content) VALUES (new.id, new.title, new.content); + END + """ + ) + conn.execute( + """ + CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN + INSERT INTO memories_fts(memories_fts, rowid, title, content) VALUES('delete', old.id, old.title, old.content); + END + """ + ) + conn.execute( + """ + CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN + INSERT INTO memories_fts(memories_fts, rowid, title, content) VALUES('delete', old.id, old.title, old.content); + INSERT INTO memories_fts(rowid, title, content) VALUES (new.id, new.title, new.content); + END + """ + ) + except Exception as fts_exc: + logger.warning( + f"SQLite FTS5 not available, falling back to substring matching: {fts_exc}" + ) + except Exception as exc: + logger.error(f"Failed to initialize local AgentMemory DB: {exc}") + + def _get_embedding(self, text: str) -> list[float]: + base_url = getattr(self.settings, "embedding_base_url", "").strip() + api_key = getattr(self.settings, "embedding_api_key", "").strip() + + if not base_url or not api_key: + default_llm = ( + config.llm.get("default") if isinstance(config.llm, dict) else None + ) + if not base_url: + base_url = ( + getattr(default_llm, "base_url", "http://localhost:1234/v1") + if default_llm + else "http://localhost:1234/v1" + ) + if not api_key: + api_key = ( + getattr(default_llm, "api_key", "lm-studio") + if default_llm + else "lm-studio" + ) + + url = base_url.rstrip("/") + "/embeddings" + model = getattr( + self.settings, "embedding_model", "text-embedding-nomic-embed-text-v1.5" + ) + payload = { + "model": model, + "input": text[:8000], + } + headers = {"Content-Type": "application/json"} + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + + req = urllib.request.Request( + url, + data=json.dumps(payload).encode("utf-8"), + headers=headers, + method="POST", + ) + with urllib.request.urlopen( + req, timeout=getattr(self.settings, "timeout_seconds", 8) + ) as resp: + res = json.loads(resp.read().decode("utf-8")) + data = res.get("data", []) + if not data or "embedding" not in data[0]: + raise ValueError("Invalid embeddings response format") + return [float(x) for x in data[0]["embedding"]] + + def _index_memory_vector( + self, memory_id: int, title: str, content: str, conversation_id: str + ) -> bool: + if getattr(self.settings, "vector_backend", "none") != "faiss": + return False + try: + import faiss + import numpy as np + except ImportError as exc: + self.last_vector_error = f"Missing FAISS or numpy: {exc}" + logger.warning(self.last_vector_error) + return False + + try: + vec = self._get_embedding(f"{title}\n{content}") + arr = np.array([vec], dtype=np.float32) + norm = np.linalg.norm(arr) + if norm > 0: + arr = arr / norm + + with self._vector_lock: + index_path = self.settings.vector_index_path + os.makedirs(os.path.dirname(index_path), exist_ok=True) + if os.path.exists(index_path): + index = faiss.read_index(index_path) + else: + dim = arr.shape[1] + index = faiss.IndexIDMap(faiss.IndexFlatIP(dim)) + + ids = np.array([memory_id], dtype=np.int64) + try: + index.remove_ids(np.array([memory_id], dtype=np.int64)) + except Exception: + pass + index.add_with_ids(arr, ids) + faiss.write_index(index, index_path) + + meta_path = self.settings.vector_meta_path + meta = {} + if os.path.exists(meta_path): + try: + with open(meta_path, "r", encoding="utf-8") as f: + meta = json.load(f) + except Exception: + meta = {} + meta[str(memory_id)] = { + "memory_id": int(memory_id), + "conversation_id": conversation_id, + "project": self.project, + "title": title, + "content": content, + } + os.makedirs(os.path.dirname(meta_path), exist_ok=True) + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + self.last_vector_error = None + return True + except Exception as exc: + self.last_vector_error = str(exc) + logger.warning(f"Failed to index memory vector for id={memory_id}: {exc}") + return False + + def remember( + self, + *, + conversation_id: str, + content: str, + title: str = "OpenManus memory", + concepts: Optional[list[str]] = None, + metadata: Optional[dict[str, Any]] = None, + ) -> bool: + if not self.enabled or not content.strip(): + return False + + memory_id: Optional[int] = None + try: + with self._get_conn() as conn: + cursor = conn.execute( + """ + INSERT INTO memories (conversation_id, project, title, content, concepts, metadata) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + conversation_id, + self.project, + title, + content, + json.dumps(concepts or []), + json.dumps(metadata or {}), + ), + ) + conn.commit() + memory_id = cursor.lastrowid + except Exception as exc: + logger.error(f"Local AgentMemory remember failed: {exc}") + return False + + if ( + getattr(self.settings, "vector_backend", "none") == "faiss" + and memory_id is not None + ): + self._index_memory_vector( + memory_id=int(memory_id), + title=title, + content=content, + conversation_id=conversation_id, + ) + return True + + def search( + self, + *, + conversation_id: str, + query: str, + limit: Optional[int] = None, + ) -> list[MemoryHit]: + if not self.enabled or not query.strip(): + return [] + + limit_val = int(limit or self.settings.top_k) + kw_hits: dict[int, MemoryHit] = {} + + try: + with self._get_conn() as conn: + try: + sanitized_words = [ + f'"{word}"' for word in query.split() if len(word) > 1 + ] + if not sanitized_words: + sanitized_words = [f'"{query}"'] + sanitized_query = " OR ".join(sanitized_words) + + cursor = conn.execute( + """ + SELECT m.id, m.title, m.content, bm25(memories_fts) as score + FROM memories m + JOIN memories_fts f ON m.id = f.rowid + WHERE m.conversation_id = ? AND m.project = ? AND memories_fts MATCH ? + ORDER BY score ASC + LIMIT ? + """, + (conversation_id, self.project, sanitized_query, limit_val * 2), + ) + rows = cursor.fetchall() + for row in rows: + score = abs(float(row["score"])) + mid = int(row["id"]) + kw_hits[mid] = MemoryHit( + title=row["title"], + content=row["content"], + score=score, + memory_id=mid, + ) + except Exception as fts_err: + logger.debug(f"FTS5 search query failed, using fallback: {fts_err}") + words = [w.lower() for w in query.split() if len(w) > 1] + if not words: + words = [query.lower()] + + cursor = conn.execute( + """ + SELECT id, title, content + FROM memories + WHERE conversation_id = ? AND project = ? + """, + (conversation_id, self.project), + ) + rows = cursor.fetchall() + + scored_rows = [] + for row in rows: + title_lower = row["title"].lower() + content_lower = row["content"].lower() + score = 0.0 + for word in words: + if word in title_lower: + score += 2.0 + if word in content_lower: + score += 1.0 + if score > 0: + scored_rows.append( + (int(row["id"]), row["title"], row["content"], score) + ) + + scored_rows.sort(key=lambda x: x[3], reverse=True) + for r_id, r_title, r_content, r_score in scored_rows[ + : limit_val * 2 + ]: + kw_hits[r_id] = MemoryHit( + title=r_title, + content=r_content, + score=r_score, + memory_id=r_id, + ) + except Exception as exc: + logger.error(f"Local AgentMemory search failed: {exc}") + + if getattr(self.settings, "vector_backend", "none") != "faiss": + hits = list(kw_hits.values()) + hits.sort(key=lambda h: h.score, reverse=True) + return hits[:limit_val] + + vec_hits: dict[int, tuple[str, str, float]] = {} + try: + import faiss + import numpy as np + + index_path = self.settings.vector_index_path + meta_path = self.settings.vector_meta_path + if os.path.exists(index_path) and os.path.exists(meta_path): + with self._vector_lock: + index = faiss.read_index(index_path) + with open(meta_path, "r", encoding="utf-8") as f: + meta_dict = json.load(f) + + if index.ntotal > 0: + q_vec = self._get_embedding(query) + arr = np.array([q_vec], dtype=np.float32) + norm = np.linalg.norm(arr) + if norm > 0: + arr = arr / norm + + k_search = min(int(index.ntotal), max(limit_val * 10, 50)) + distances, ids = index.search(arr, k_search) + for dist, mid in zip(distances[0], ids[0]): + mid_int = int(mid) + if mid_int < 0: + continue + meta = meta_dict.get(str(mid_int)) + if not meta: + continue + if str(meta.get("conversation_id")) == str( + conversation_id + ) and str(meta.get("project")) == str(self.project): + vec_hits[mid_int] = ( + meta.get("title", ""), + meta.get("content", ""), + float(dist), + ) + self.last_vector_error = None + except Exception as exc: + self.last_vector_error = str(exc) + logger.warning(f"FAISS vector search failed, falling back to SQLite: {exc}") + hits = list(kw_hits.values()) + hits.sort(key=lambda h: h.score, reverse=True) + return hits[:limit_val] + + all_ids = set(kw_hits.keys()) | set(vec_hits.keys()) + if not all_ids: + return [] + + max_kw = max([h.score for h in kw_hits.values()] + [1e-6]) + hybrid_enabled = getattr(self.settings, "hybrid_search", True) + kw_w = getattr(self.settings, "keyword_weight", 0.35) + vec_w = getattr(self.settings, "vector_weight", 0.65) + + merged: list[MemoryHit] = [] + for mid in all_ids: + kw_hit = kw_hits.get(mid) + vec_hit = vec_hits.get(mid) + + title = kw_hit.title if kw_hit else (vec_hit[0] if vec_hit else "") + content = kw_hit.content if kw_hit else (vec_hit[1] if vec_hit else "") + + norm_kw = (kw_hit.score / max_kw) if kw_hit else 0.0 + norm_vec = max(0.0, vec_hit[2]) if vec_hit else 0.0 + + if hybrid_enabled: + combined_score = kw_w * norm_kw + vec_w * norm_vec + else: + combined_score = norm_vec if vec_hit else norm_kw + + merged.append( + MemoryHit( + title=title, content=content, score=combined_score, memory_id=mid + ) + ) + + merged.sort(key=lambda h: h.score, reverse=True) + return merged[:limit_val] + + def rebuild_vector_index(self) -> int: + if getattr(self.settings, "vector_backend", "none") != "faiss": + return 0 + try: + import faiss + import numpy as np + except ImportError as exc: + self.last_vector_error = str(exc) + logger.warning(f"Missing dependencies for rebuild_vector_index: {exc}") + return 0 + + try: + with self._get_conn() as conn: + cursor = conn.execute( + """ + SELECT id, conversation_id, project, title, content + FROM memories + """ + ) + rows = cursor.fetchall() + except Exception as exc: + logger.error(f"Failed to fetch memories for vector index rebuild: {exc}") + return 0 + + if not rows: + with self._vector_lock: + index_path = self.settings.vector_index_path + meta_path = self.settings.vector_meta_path + if os.path.exists(index_path): + try: + os.remove(index_path) + except Exception: + pass + if os.path.exists(meta_path): + try: + os.remove(meta_path) + except Exception: + pass + return 0 + + embeddings = [] + ids = [] + meta_dict = {} + + for row in rows: + mid = int(row["id"]) + title = row["title"] + content = row["content"] + cid = row["conversation_id"] + proj = row["project"] + try: + vec = self._get_embedding(f"{title}\n{content}") + arr = np.array(vec, dtype=np.float32) + norm = np.linalg.norm(arr) + if norm > 0: + arr = arr / norm + embeddings.append(arr) + ids.append(mid) + meta_dict[str(mid)] = { + "memory_id": mid, + "conversation_id": cid, + "project": proj, + "title": title, + "content": content, + } + except Exception as exc: + logger.warning( + f"Failed embedding for memory id={mid} during rebuild: {exc}" + ) + + if not embeddings: + return 0 + + emb_matrix = np.array(embeddings, dtype=np.float32) + id_array = np.array(ids, dtype=np.int64) + dim = emb_matrix.shape[1] + + with self._vector_lock: + index_path = self.settings.vector_index_path + meta_path = self.settings.vector_meta_path + os.makedirs(os.path.dirname(index_path), exist_ok=True) + os.makedirs(os.path.dirname(meta_path), exist_ok=True) + + base_index = faiss.IndexFlatIP(dim) + index = faiss.IndexIDMap(base_index) + index.add_with_ids(emb_matrix, id_array) + faiss.write_index(index, index_path) + + with open(meta_path, "w", encoding="utf-8") as f: + json.dump(meta_dict, f, ensure_ascii=False, indent=2) + + self.last_vector_error = None + return len(ids) + + def get_vector_health(self) -> dict[str, Any]: + backend = getattr(self.settings, "vector_backend", "none") + provider = getattr(self.settings, "embedding_provider", "openai_compatible") + res = { + "vector_backend": backend, + "embedding_provider": provider, + "vector_live": False, + "vector_count": 0, + "last_vector_error": getattr(self, "last_vector_error", None), + } + if not self.enabled or backend != "faiss": + return res + try: + import faiss + + index_path = self.settings.vector_index_path + if os.path.exists(index_path): + with self._vector_lock: + index = faiss.read_index(index_path) + res["vector_count"] = int(index.ntotal) + res["vector_live"] = True + else: + res["vector_count"] = 0 + res["vector_live"] = True + except Exception as exc: + res["vector_live"] = False + res["last_vector_error"] = str(exc) + return res + + def format_context( + self, + *, + conversation_id: str, + query: str, + limit: Optional[int] = None, + ) -> str: + hits = self.search(conversation_id=conversation_id, query=query, limit=limit) + if not hits: + return "" + lines = [ + "AgentMemory recall context:", + "- Relevant long-term memory snippets for this conversation:", + ] + for idx, hit in enumerate(hits, start=1): + lines.append( + f" {idx}. {hit.title} (score={hit.score:.3f}): {hit.content[:700]}" + ) + return "\n".join(lines) + + +agentmemory = AgentMemoryClient() diff --git a/app/prompt/manus.py b/app/prompt/manus.py index 99e7e8315..b41dd5545 100644 --- a/app/prompt/manus.py +++ b/app/prompt/manus.py @@ -1,10 +1,41 @@ SYSTEM_PROMPT = ( "You are OpenManus, an all-capable AI assistant, aimed at solving any task presented by the user. You have various tools at your disposal that you can call upon to efficiently complete complex requests. Whether it's programming, information retrieval, file processing, web browsing, or human interaction (only for extreme cases), you can handle it all." - "The initial directory is: {directory}" + " The initial directory is: {directory}" + " IMPORTANT: Always work autonomously. Never ask the user for clarification, more details, or confirmation. If you are unsure about any detail, make the most reasonable assumption and proceed. Complete the task fully without pausing for user input." + " Work like a careful coding agent: understand the current workspace, make a brief internal plan, use tools deliberately, inspect results, and verify the final deliverable before finishing." + " Use built-in skill playbooks and fast codebase tools when they fit the task." ) NEXT_STEP_PROMPT = """ -Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps. +Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, first inspect relevant files and current workspace state, then break down the problem and use different tools step by step to solve it. After using each tool, interpret the result, decide the next best action, and avoid repeating prior failed steps. -If you want to stop the interaction at any point, use the `terminate` tool/function call. +Think in this operating loop: +1. Understand: identify the user goal, current workspace state, and likely deliverables. +2. Plan: choose a short sequence of tool-backed actions. +3. Execute: use bash/python/file/browser/search tools as needed. +4. Verify: run checks, inspect generated files, confirm artifacts exist, and read errors/logs when something fails. +5. Finish only when the task is actually complete or when you can clearly report what blocked completion. + +Tool strategy: +- Prefer batching independent reads/searches in a single step (for example `glob` + `grep` + `read_files`) instead of issuing one tiny tool call per step. +- When multiple independent checks are needed, return multiple tool calls in one response to reduce total step count. +- For coding/debugging tasks, start with `skill_playbook` when a specialized workflow applies, then use `codebase_overview`, `glob`, `grep`, and `read_files` to inspect quickly. +- Use `planning` for multi-step or risky work and update the plan as steps complete. +- **Code editing hierarchy (most reliable → least):** + 1. `line_edit` — **preferred for all single-file edits**. Always `cat -n ` or `str_replace_editor view` first to get exact line numbers, then call `line_edit` with `start_line`, `end_line`, and `new_content`. No string matching = never fails on whitespace/duplicates. + 2. `apply_patch_editor` — use for **multi-file atomic changes** or when a unified diff is already available. + 3. `str_replace_editor str_replace` — last resort only; requires exact whitespace match and unique occurrence. + - Never attempt str_replace on a string that appears more than once, or when the file content was not freshly read. +- Prefer dedicated `glob`/`grep`/`read_files` over ad hoc `find`, `grep`, or `cat` shell commands unless shell behavior is specifically needed. +- Use `web_search` or browser tools for current external facts, documentation, and web tasks. + +Do not ask the user for clarification, confirmation, or missing details. If something is uncertain, make the most reasonable assumption and continue. If optional mid-task input may help, you may briefly wait for it with a bounded timeout, but you must proceed autonomously if no message arrives. + +Before finishing, verify that requested output files actually exist in the workspace. For LaTeX/PDF tasks, compile the document, check that the PDF was created, and if it was not created, inspect and report the relevant `.log` errors instead of claiming success. + +When ending, do not just stop. Provide a concise final answer or call `terminate` with `status`, `summary`, and `reason`. The summary should say what was done, where the user can inspect it, and any remaining limitations. If a command/API/model fails, end with failure and the exact error after one useful recovery attempt. + +Use the shared conversation workspace as persistent project memory. Continue from existing files and previous task context; do not create unrelated duplicate workspaces or restart from scratch unless explicitly requested. + +If you want to stop the interaction at any point, use the `terminate` tool/function call with a useful summary and reason. """ diff --git a/app/runtime_settings.py b/app/runtime_settings.py new file mode 100644 index 000000000..42c667fe3 --- /dev/null +++ b/app/runtime_settings.py @@ -0,0 +1,101 @@ +import os +import time +from typing import Any, Optional + +from sqlalchemy import create_engine, text + + +DATABASE_URL = os.getenv( + "DATABASE_URL", + "postgresql+psycopg2://postgres:postgres@localhost:5432/openmanus", +) + +_engine = None +_cache: dict[str, tuple[float, Any]] = {} +_CACHE_SECONDS = 5 + + +def _get_engine(): + global _engine + if _engine is None: + _engine = create_engine(DATABASE_URL) + return _engine + + +def get_setting(key: str, default: Optional[Any] = None) -> Any: + now = time.time() + cached = _cache.get(key) + if cached and now - cached[0] < _CACHE_SECONDS: + return cached[1] + + try: + with _get_engine().begin() as connection: + value = connection.execute( + text("SELECT value FROM app_settings WHERE key = :key"), + {"key": key}, + ).scalar() + except Exception: + value = None + + if value is None: + value = default + _cache[key] = (now, value) + return value + + +def get_disabled_tools() -> set[str]: + tools = get_setting("tools", {}) + disabled = tools.get("disabled", []) if isinstance(tools, dict) else [] + return {str(name) for name in disabled} + + +def get_llm_connection() -> dict: + settings = get_setting("llm_connection", {}) + if not isinstance(settings, dict): + return {} + + cleaned = dict(settings) + + def _is_masked(value: Any) -> bool: + return isinstance(value, str) and value.strip() == "********" + + # Never pass redacted placeholders into runtime code paths. + for key in ("api_key", "base_url", "model", "api_type"): + if _is_masked(cleaned.get(key)): + cleaned.pop(key, None) + + for key in ("max_tokens", "max_input_tokens"): + value = cleaned.get(key) + if _is_masked(value): + cleaned.pop(key, None) + continue + if isinstance(value, str): + try: + cleaned[key] = int(value) + except ValueError: + cleaned.pop(key, None) + + temp = cleaned.get("temperature") + if _is_masked(temp): + cleaned.pop("temperature", None) + elif isinstance(temp, str): + try: + cleaned["temperature"] = float(temp) + except ValueError: + cleaned.pop("temperature", None) + + return cleaned + + +def get_config_overrides() -> dict: + settings = get_setting("config_overrides", {}) + return settings if isinstance(settings, dict) else {} + + +def get_config_override(path: str, default: Any = None) -> Any: + value: Any = get_config_overrides() + for part in path.split("."): + if not isinstance(value, dict) or part not in value: + return default + value = value[part] + return value diff --git a/app/sandbox/conversation.py b/app/sandbox/conversation.py new file mode 100644 index 000000000..5004d0dcc --- /dev/null +++ b/app/sandbox/conversation.py @@ -0,0 +1,431 @@ +"""Persistent per-conversation Docker sandbox runtime.""" + +from __future__ import annotations + +import asyncio +import io +import os +import re +import shlex +import tarfile +from pathlib import Path +from typing import Any, Optional + +import docker +from docker.errors import APIError, NotFound + +from app.config import SandboxSettings + + +class ConversationSandbox: + """A Docker-backed computer for one conversation. + + The container persists across follow-up tasks and mounts exactly one + conversation workspace at /workspace so artifacts remain visible to the UI. + """ + + def __init__( + self, + conversation_id: str, + host_workspace: Path, + config: SandboxSettings, + ) -> None: + self.conversation_id = conversation_id + self.host_workspace = host_workspace.resolve() + self.config = config + self.client = docker.from_env() + self.api = self.client.api + self.container = None + safe_id = re.sub(r"[^a-zA-Z0-9_.-]", "_", conversation_id)[:80] + self.name = f"openmanus_sandbox_{safe_id}" + + async def ensure(self) -> "ConversationSandbox": + self.host_workspace.mkdir(parents=True, exist_ok=True) + try: + self.container = await asyncio.to_thread( + self.client.containers.get, self.name + ) + await self._start_if_needed() + return self + except NotFound: + pass + + return await self._create_container() + + async def _create_container(self, _retried: bool = False) -> "ConversationSandbox": + """Create a fresh sandbox container, auto-removing stale conflicts.""" + binds = { + str(self.host_workspace): { + "bind": self.config.work_dir, + "mode": "rw", + } + } + if self.config.docker_socket_enabled and os.path.exists("/var/run/docker.sock"): + binds["/var/run/docker.sock"] = { + "bind": "/var/run/docker.sock", + "mode": "rw", + } + host_config = self.api.create_host_config( + mem_limit=self.config.memory_limit, + cpu_period=100000, + cpu_quota=int(100000 * self.config.cpu_limit), + network_mode="bridge" if self.config.network_enabled else "none", + binds=binds, + ) + try: + container_data = await asyncio.to_thread( + self.api.create_container, + image=self.config.image, + command="tail -f /dev/null", + hostname="openmanus-sandbox", + working_dir=self.config.work_dir, + environment={"DOCKER_HOST": "unix:///var/run/docker.sock"}, + host_config=host_config, + name=self.name, + labels={ + "openmanus.kind": "conversation-sandbox", + "openmanus.conversation_id": self.conversation_id, + }, + tty=True, + detach=True, + ) + except APIError as exc: + if exc.status_code == 409 and not _retried: + # Stale container with the same name – remove it and retry once. + try: + stale = await asyncio.to_thread( + self.client.containers.get, self.name + ) + await asyncio.to_thread(stale.remove, force=True) + except Exception: + pass + return await self._create_container(_retried=True) + raise + self.container = self.client.containers.get(container_data["Id"]) + await asyncio.to_thread(self.container.start) + return self + + async def status(self) -> dict[str, Any]: + try: + self.container = await asyncio.to_thread( + self.client.containers.get, self.name + ) + except NotFound: + return {"exists": False, "status": "missing", "container": None} + await asyncio.to_thread(self.container.reload) + return { + "exists": True, + "status": self.container.status, + "container": { + "id": self.container.short_id, + "name": self.container.name, + "image": ",".join(self.container.image.tags) + if self.container.image.tags + else self.container.image.short_id, + }, + } + + async def pause(self) -> None: + await self.ensure() + await asyncio.to_thread(self.container.pause) + + async def resume(self) -> None: + try: + self.container = await asyncio.to_thread( + self.client.containers.get, self.name + ) + except NotFound: + await self.ensure() + return + await asyncio.to_thread(self.container.reload) + if self.container.status == "paused": + await asyncio.to_thread(self.container.unpause) + elif self.container.status != "running": + await asyncio.to_thread(self.container.start) + + async def delete(self) -> None: + try: + self.container = await asyncio.to_thread( + self.client.containers.get, self.name + ) + except NotFound: + return + await asyncio.to_thread(self.container.remove, force=True) + + async def _start_if_needed(self) -> None: + assert self.container is not None + await asyncio.to_thread(self.container.reload) + if self.container.status == "paused": + await asyncio.to_thread(self.container.unpause) + return + if self.container.status != "running": + await asyncio.to_thread(self.container.start) + + def container_path(self, path: str | os.PathLike[str]) -> str: + raw = str(path) + if raw.startswith(str(self.host_workspace)): + rel = os.path.relpath(raw, self.host_workspace) + return os.path.join(self.config.work_dir, rel) + if raw == "/workspace" or raw.startswith("/workspace/"): + return raw + if os.path.isabs(raw): + return raw + return os.path.join(self.config.work_dir, raw) + + async def run( + self, + command: str, + timeout: Optional[int] = None, + task: Optional[Any] = None, + tool_call: Optional[dict] = None, + tool_name: str = "bash", + ) -> tuple[int, str, str]: + await self.ensure() + seconds = int(timeout or self.config.timeout or 300) + wrapped = f"timeout -k 5s {seconds}s bash -lc {shlex.quote(command)}" + + def _run() -> tuple[int, str, str]: + exec_data = self.api.exec_create( + self.container.id, + ["bash", "-lc", wrapped], + stdout=True, + stderr=True, + tty=False, + workdir=self.config.work_dir, + environment={"PYTHONUNBUFFERED": "1"}, + ) + exec_id = exec_data["Id"] + stdout_parts: list[str] = [] + stderr_parts: list[str] = [] + for stdout, stderr in self.api.exec_start(exec_id, stream=True, demux=True): + if stdout: + text = stdout.decode("utf-8", errors="replace") + stdout_parts.append(text) + if task is not None: + task.emit( + "terminal_output", + { + "id": (tool_call or {}).get("id"), + "name": (tool_call or {}).get("name", tool_name), + "stream": "stdout", + "chunk": text, + }, + ) + if stderr: + text = stderr.decode("utf-8", errors="replace") + stderr_parts.append(text) + if task is not None: + task.emit( + "terminal_output", + { + "id": (tool_call or {}).get("id"), + "name": (tool_call or {}).get("name", tool_name), + "stream": "stderr", + "chunk": text, + }, + ) + inspect = self.api.exec_inspect(exec_id) + return ( + int(inspect.get("ExitCode") or 0), + "".join(stdout_parts), + "".join(stderr_parts), + ) + + return await asyncio.to_thread(_run) + + async def read_file(self, path: str | os.PathLike[str]) -> str: + container_path = self.container_path(path) + code, stdout, stderr = await self.run( + f"cat {shlex.quote(container_path)}", timeout=30 + ) + if code != 0: + raise FileNotFoundError(stderr or f"Could not read {container_path}") + return stdout + + async def write_file(self, path: str | os.PathLike[str], content: str) -> None: + container_path = self.container_path(path) + parent_dir = os.path.dirname(container_path) or self.config.work_dir + await self.run(f"mkdir -p {shlex.quote(parent_dir)}", timeout=30) + + def _write() -> None: + data = content.encode("utf-8") + tar_stream = io.BytesIO() + with tarfile.open(fileobj=tar_stream, mode="w") as tar: + tarinfo = tarfile.TarInfo(name=os.path.basename(container_path)) + tarinfo.size = len(data) + tar.addfile(tarinfo, io.BytesIO(data)) + tar_stream.seek(0) + self.container.put_archive(parent_dir, tar_stream.read()) + + await asyncio.to_thread(_write) + + def host_path_for_container_path(self, path: str) -> Optional[Path]: + if path == self.config.work_dir: + return self.host_workspace + prefix = self.config.work_dir.rstrip("/") + "/" + if path.startswith(prefix): + rel = path[len(prefix) :] + return self.host_workspace / rel + return None + + async def exists(self, path: str | os.PathLike[str]) -> bool: + code, _, _ = await self.run( + f"test -e {shlex.quote(self.container_path(path))}", timeout=10 + ) + return code == 0 + + async def is_directory(self, path: str | os.PathLike[str]) -> bool: + code, _, _ = await self.run( + f"test -d {shlex.quote(self.container_path(path))}", timeout=10 + ) + return code == 0 + + async def list_processes(self) -> list[dict[str, Any]]: + """Return processes running inside this conversation container.""" + await self.ensure() + code, stdout, _ = await self.run( + "ps -eo pid=,ppid=,stat=,etime=,comm=,args= | sed '/ ps -eo /d'", + timeout=10, + ) + if code != 0: + return [] + listen_ports = await self._list_listening_ports() + + processes: list[dict[str, Any]] = [] + for line in stdout.splitlines(): + parts = line.strip().split(None, 5) + if len(parts) < 6: + continue + pid, ppid, stat, elapsed, command, args = parts + if command in {"ps", "sed"}: + continue + processes.append( + { + "pid": int(pid), + "ppid": int(ppid), + "stat": stat, + "elapsed": elapsed, + "command": command, + "args": args, + "ports": listen_ports.get(int(pid), []), + "zombie": "Z" in stat, + "protected": int(pid) == 1 + or args == "tail -f /dev/null" + or "Z" in stat, + } + ) + return processes + + async def list_exposed_urls(self) -> list[dict[str, Any]]: + """Return likely HTTP URLs exposed by processes in the sandbox.""" + await self.ensure() + await asyncio.to_thread(self.container.reload) + attrs = self.container.attrs or {} + networks = attrs.get("NetworkSettings", {}).get("Networks", {}) + ip_address = "" + for network in networks.values(): + ip_address = network.get("IPAddress") or ip_address + ports_by_pid = await self._list_listening_ports() + processes = await self.list_processes() + command_by_pid = {item["pid"]: item for item in processes} + urls: list[dict[str, Any]] = [] + seen: set[str] = set() + for pid, ports in ports_by_pid.items(): + process = command_by_pid.get(pid, {}) + for port in ports: + if port in seen: + continue + seen.add(port) + urls.append( + { + "port": port, + "url": f"http://{ip_address}:{port}/" if ip_address else "", + "pid": pid, + "command": process.get("command", ""), + "args": process.get("args", ""), + } + ) + return urls + + async def _list_listening_ports(self) -> dict[int, list[str]]: + code, stdout, _ = await self.run("ss -tlnp 2>/dev/null || true", timeout=10) + if code != 0: + return {} + ports_by_pid: dict[int, list[str]] = {} + for line in stdout.splitlines(): + if "pid=" not in line or "LISTEN" not in line: + continue + pid_match = re.search(r"pid=(\d+)", line) + address = line.split()[3] if len(line.split()) > 3 else "" + port_match = re.search(r":(\d+)$", address) + if not pid_match or not port_match: + continue + ports_by_pid.setdefault(int(pid_match.group(1)), []).append( + port_match.group(1) + ) + return ports_by_pid + + async def kill_process(self, pid: int, signal: str = "TERM") -> None: + """Kill one process inside this conversation container.""" + if pid <= 1: + raise ValueError("Refusing to kill the sandbox init process") + safe_signal = re.sub(r"[^A-Z0-9]", "", signal.upper()) or "TERM" + code, _, stderr = await self.run( + f"kill -s {shlex.quote(safe_signal)} {int(pid)}", timeout=10 + ) + if code != 0: + raise RuntimeError(stderr or f"Could not kill process {pid}") + + async def list_docker_containers(self) -> list[dict[str, Any]]: + """Return Docker containers visible from the sandbox Docker socket.""" + await self.ensure() + command = "docker ps --format " + shlex.quote( + "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Command}}" + ) + code, stdout, _ = await self.run(command, timeout=15) + if code != 0: + return [] + + containers: list[dict[str, Any]] = [] + for line in stdout.splitlines(): + parts = line.split("\t") + if len(parts) < 5: + continue + container_id, name, image, status, command_text = parts[:5] + protected = ( + name.startswith("openmanus-") + or name == self.name + or name.startswith("openmanus_sandbox_") + ) + containers.append( + { + "id": container_id, + "name": name, + "image": image, + "status": status, + "command": command_text, + "protected": protected, + } + ) + return containers + + async def stop_docker_container(self, container_id: str) -> None: + """Stop a Docker container visible from the sandbox socket.""" + containers = await self.list_docker_containers() + target = next( + ( + item + for item in containers + if item["id"].startswith(container_id) or item["name"] == container_id + ), + None, + ) + if target is None: + raise ValueError("Container not found") + if target.get("protected"): + raise ValueError("Refusing to stop an OpenManus system container") + code, _, stderr = await self.run( + f"docker stop {shlex.quote(container_id)}", timeout=30 + ) + if code != 0: + raise RuntimeError(stderr or f"Could not stop container {container_id}") diff --git a/app/sandbox/core/sandbox.py b/app/sandbox/core/sandbox.py index c57b3f239..0886e171c 100644 --- a/app/sandbox/core/sandbox.py +++ b/app/sandbox/core/sandbox.py @@ -118,6 +118,12 @@ def _prepare_volume_bindings(self) -> Dict[str, Dict[str, str]]: for host_path, container_path in self.volume_bindings.items(): bindings[host_path] = {"bind": container_path, "mode": "rw"} + # Add docker socket if enabled + if getattr(self.config, "docker_socket_enabled", False): + docker_socket = "/var/run/docker.sock" + if os.path.exists(docker_socket): + bindings[docker_socket] = {"bind": docker_socket, "mode": "rw"} + return bindings @staticmethod diff --git a/app/skills.py b/app/skills.py new file mode 100644 index 000000000..cb4a859a0 --- /dev/null +++ b/app/skills.py @@ -0,0 +1,190 @@ +"""OpenHands-compatible skill discovery for OpenManus. + +Skills are Markdown files with optional YAML frontmatter. Repo or workspace +owners can place them under `.openhands/skills` or `.openhands/microagents`. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable, Optional + +import yaml + + +@dataclass +class Skill: + name: str + path: str + body: str + type: str = "knowledge" + version: str = "1.0" + agent: str = "Manus" + triggers: list[str] | None = None + + def matches(self, text: str) -> bool: + haystack = text.lower() + return any(trigger.lower() in haystack for trigger in self.triggers or []) + + def summary(self) -> dict: + return { + "name": self.name, + "path": self.path, + "type": self.type, + "version": self.version, + "agent": self.agent, + "triggers": self.triggers or [], + } + + +def _parse_skill(path: Path) -> Skill | None: + try: + raw = path.read_text(encoding="utf-8") + except OSError: + return None + + metadata: dict = {} + body = raw + if raw.startswith("---"): + parts = raw.split("---", 2) + if len(parts) >= 3: + try: + metadata = yaml.safe_load(parts[1]) or {} + except yaml.YAMLError: + metadata = {} + body = parts[2].strip() + + name = str(metadata.get("name") or path.stem).strip() + triggers = metadata.get("triggers") or [] + if isinstance(triggers, str): + triggers = [triggers] + return Skill( + name=name, + path=str(path), + body=body.strip(), + type=str(metadata.get("type") or "knowledge"), + version=str(metadata.get("version") or "1.0"), + agent=str(metadata.get("agent") or "Manus"), + triggers=[str(item) for item in triggers], + ) + + +def skill_roots( + workspace_root: str | Path | None = None, include_vendor: bool = True +) -> list[Path]: + roots = [ + Path.cwd() / ".openhands" / "skills", + Path.cwd() / ".openhands" / "microagents", + Path.cwd() / "skills", + ] + if include_vendor: + roots.extend( + [ + Path.cwd() / "vendor" / "everything-claude-code" / "skills", + Path.cwd() / "vendor" / "everything-claude-code" / "agents-skills", + Path.cwd() / "vendor" / "everything-claude-code" / "agents", + ] + ) + if workspace_root: + workspace = Path(workspace_root) + roots.extend( + [ + workspace / ".openhands" / "skills", + workspace / ".openhands" / "microagents", + workspace / "skills", + ] + ) + if include_vendor: + roots.extend( + [ + workspace / "vendor" / "everything-claude-code" / "skills", + workspace / "vendor" / "everything-claude-code" / "agents-skills", + workspace / "vendor" / "everything-claude-code" / "agents", + ] + ) + return roots + + +def load_skills( + workspace_root: str | Path | None = None, + include_vendor: bool = True, + disabled_skills: Optional[set[str]] = None, +) -> list[Skill]: + seen: set[Path] = set() + skills: list[Skill] = [] + blocked = {name.lower() for name in (disabled_skills or set())} + for root in skill_roots(workspace_root, include_vendor=include_vendor): + if not root.exists(): + continue + for path in sorted(root.rglob("*.md")): + resolved = path.resolve() + if resolved in seen: + continue + seen.add(resolved) + skill = _parse_skill(path) + if skill is not None: + if skill.name.lower() in blocked: + continue + skills.append(skill) + return skills + + +def select_skills( + prompt: str, + workspace_root: str | Path | None = None, + limit: int = 6, + include_vendor: bool = True, + disabled_skills: Optional[set[str]] = None, +) -> list[Skill]: + skills = load_skills( + workspace_root, + include_vendor=include_vendor, + disabled_skills=disabled_skills, + ) + prompt_text = (prompt or "").lower() + prompt_tokens = { + token + for token in "".join(ch if ch.isalnum() else " " for ch in prompt_text).split() + if len(token) >= 3 + } + + matched = [skill for skill in skills if skill.triggers and skill.matches(prompt)] + + # Fallback relevance for trigger-less/vendor skills: only include when the + # prompt text overlaps with skill name/path keywords. + ranked_fallback: list[tuple[int, Skill]] = [] + for skill in skills: + if skill in matched: + continue + label = f"{skill.name} {Path(skill.path).stem}".lower() + skill_tokens = { + token + for token in "".join(ch if ch.isalnum() else " " for ch in label).split() + if len(token) >= 3 + } + overlap = prompt_tokens & skill_tokens + if overlap: + ranked_fallback.append((len(overlap), skill)) + ranked_fallback.sort(key=lambda item: item[0], reverse=True) + + selected: list[Skill] = [] + for skill in [*matched, *[item[1] for item in ranked_fallback]]: + if skill.name in {item.name for item in selected}: + continue + selected.append(skill) + if len(selected) >= limit: + break + return selected + + +def format_skill_context(skills: Iterable[Skill]) -> str: + chunks = [] + for skill in skills: + chunks.append(f"Skill: {skill.name}\n{skill.body}") + if not chunks: + return "" + return ( + "Relevant OpenHands-style skills loaded for this conversation:\n\n" + + "\n\n---\n\n".join(chunks) + ) diff --git a/app/task_context.py b/app/task_context.py new file mode 100644 index 000000000..9e7bc867c --- /dev/null +++ b/app/task_context.py @@ -0,0 +1,60 @@ +from contextvars import ContextVar +from typing import Any, Optional + + +current_task: ContextVar[Optional[Any]] = ContextVar("current_task", default=None) +current_tool_call: ContextVar[Optional[dict]] = ContextVar( + "current_tool_call", default=None +) +current_sandbox: ContextVar[Optional[Any]] = ContextVar("current_sandbox", default=None) +current_workspace: ContextVar[Optional[str]] = ContextVar( + "current_workspace", default=None +) +current_model: ContextVar[Optional[str]] = ContextVar("current_model", default=None) +current_llm_connection: ContextVar[Optional[dict]] = ContextVar( + "current_llm_connection", default=None +) +current_requested_context_window: ContextVar[Optional[int]] = ContextVar( + "current_requested_context_window", default=None +) +current_auto_context_compress: ContextVar[bool] = ContextVar( + "current_auto_context_compress", default=True +) + + +def get_current_task() -> Optional[Any]: + return current_task.get() + + +def emit_current_task(event_type: str, data: dict) -> None: + task = get_current_task() + if task is not None: + task.emit(event_type, data) + + +def get_current_tool_call() -> Optional[dict]: + return current_tool_call.get() + + +def get_current_sandbox() -> Optional[Any]: + return current_sandbox.get() + + +def get_current_workspace() -> Optional[str]: + return current_workspace.get() + + +def get_current_model() -> Optional[str]: + return current_model.get() + + +def get_current_llm_connection() -> Optional[dict]: + return current_llm_connection.get() + + +def get_current_requested_context_window() -> Optional[int]: + return current_requested_context_window.get() + + +def get_current_auto_context_compress() -> bool: + return current_auto_context_compress.get() diff --git a/app/tool/__init__.py b/app/tool/__init__.py index 636e9b8da..943a9cb88 100644 --- a/app/tool/__init__.py +++ b/app/tool/__init__.py @@ -1,24 +1,40 @@ +from app.tool.apply_patch_editor import ApplyPatchEditor from app.tool.base import BaseTool from app.tool.bash import Bash from app.tool.browser_use_tool import BrowserUseTool +from app.tool.codebase import CodebaseOverview, GlobSearch, GrepSearch, ReadFiles from app.tool.crawl4ai import Crawl4aiTool from app.tool.create_chat_completion import CreateChatCompletion +from app.tool.line_edit import LineEdit +from app.tool.long_term_memory import MemoryRecall, MemorySave from app.tool.planning import PlanningTool +from app.tool.skill_playbook import SkillPlaybook from app.tool.str_replace_editor import StrReplaceEditor from app.tool.terminate import Terminate from app.tool.tool_collection import ToolCollection +from app.tool.user_input_tool import WaitForUserInput from app.tool.web_search import WebSearch __all__ = [ "BaseTool", + "ApplyPatchEditor", "Bash", "BrowserUseTool", + "CodebaseOverview", + "GlobSearch", + "GrepSearch", + "ReadFiles", + "LineEdit", "Terminate", "StrReplaceEditor", "WebSearch", "ToolCollection", "CreateChatCompletion", "PlanningTool", + "SkillPlaybook", "Crawl4aiTool", + "WaitForUserInput", + "MemorySave", + "MemoryRecall", ] diff --git a/app/tool/apply_patch_editor.py b/app/tool/apply_patch_editor.py new file mode 100644 index 000000000..7cba73a5b --- /dev/null +++ b/app/tool/apply_patch_editor.py @@ -0,0 +1,185 @@ +"""Unified-diff patch editor tool for reliable file updates.""" + +from __future__ import annotations + +import subprocess +import tempfile +from pathlib import Path +from typing import Any + +from app.config import config +from app.exceptions import ToolError +from app.task_context import ( + emit_current_task, + get_current_tool_call, + get_current_workspace, +) +from app.tool.base import BaseTool + + +class ApplyPatchEditor(BaseTool): + name: str = "apply_patch_editor" + description: str = ( + "Apply a unified diff patch atomically across one or more files. " + "Preferred for robust code edits over string replacement." + ) + parameters: dict = { + "type": "object", + "properties": { + "patch": { + "type": "string", + "description": ( + "Unified diff patch text. Example headers: " + "--- a/file.py and +++ b/file.py, followed by @@ hunks." + ), + }, + "check_only": { + "type": "boolean", + "description": "If true, validate patch applicability without applying it.", + }, + }, + "required": ["patch"], + } + + def _workspace_root(self) -> Path: + active = get_current_workspace() + if active: + return Path(active) + return Path(str(config.workspace_root)) + + def _run_git_apply(self, patch_path: Path, *, check_only: bool, cwd: Path) -> str: + base_cmd = [ + "git", + "apply", + "--recount", + "--whitespace=nowarn", + "--unsafe-paths", + "--no-index", + ] + check_cmd = [*base_cmd, "--check", str(patch_path)] + proc_check = subprocess.run( + check_cmd, + cwd=str(cwd), + text=True, + capture_output=True, + ) + if proc_check.returncode != 0: + detail = (proc_check.stderr or proc_check.stdout or "").strip() + raise ToolError(f"Patch check failed: {detail}") + if check_only: + return "Patch check passed (no changes applied)." + + apply_cmd = [*base_cmd, str(patch_path)] + proc_apply = subprocess.run( + apply_cmd, + cwd=str(cwd), + text=True, + capture_output=True, + ) + if proc_apply.returncode != 0: + detail = (proc_apply.stderr or proc_apply.stdout or "").strip() + raise ToolError(f"Patch apply failed: {detail}") + return "Patch applied successfully." + + @staticmethod + def _parse_patch_files(patch_text: str) -> list[dict[str, Any]]: + files: list[dict[str, Any]] = [] + current: dict[str, Any] | None = None + + for raw in patch_text.splitlines(): + line = raw.rstrip("\n") + if line.startswith("diff --git "): + if current: + files.append(current) + current = {"path": "", "added": 0, "deleted": 0, "lines": []} + continue + + if line.startswith("+++ "): + if current is None: + current = {"path": "", "added": 0, "deleted": 0, "lines": []} + path = line[4:].strip() + if path.startswith("b/"): + path = path[2:] + current["path"] = path + current["lines"].append(line) + continue + + if current is None: + continue + + if line.startswith("@@") or line.startswith("--- "): + current["lines"].append(line) + continue + + if line.startswith("+") and not line.startswith("+++"): + current["added"] += 1 + current["lines"].append(line) + continue + + if line.startswith("-") and not line.startswith("---"): + current["deleted"] += 1 + current["lines"].append(line) + continue + + if current: + files.append(current) + + normalized: list[dict[str, Any]] = [] + for f in files: + lines = f.get("lines", []) + clipped = lines[:120] + if len(lines) > 120: + clipped.append("... (diff truncated)") + normalized.append( + { + "path": f.get("path") or "(unknown)", + "added": int(f.get("added", 0)), + "deleted": int(f.get("deleted", 0)), + "diff_preview": {"command": "apply_patch", "lines": clipped}, + } + ) + return normalized + + async def execute( + self, *, patch: str, check_only: bool = False, **kwargs: Any + ) -> str: + patch_text = str(patch or "").strip() + if not patch_text: + raise ToolError("Parameter `patch` is required and must be non-empty.") + + workspace_root = self._workspace_root() + workspace_root.mkdir(parents=True, exist_ok=True) + + with tempfile.NamedTemporaryFile( + mode="w", encoding="utf-8", suffix=".patch", delete=False + ) as tmp: + tmp.write(patch_text) + tmp_path = Path(tmp.name) + + try: + result = self._run_git_apply( + tmp_path, check_only=bool(check_only), cwd=workspace_root + ) + finally: + try: + tmp_path.unlink(missing_ok=True) + except Exception: + pass + + if not check_only: + tool_call = get_current_tool_call() or {} + tool_call_id = str(tool_call.get("id") or "") + for item in self._parse_patch_files(patch_text): + emit_current_task( + "workspace_file_updated", + { + "tool_call_id": tool_call_id, + "tool": self.name, + "path": item["path"], + "added_lines": item["added"], + "deleted_lines": item["deleted"], + "diff_preview": item["diff_preview"], + }, + ) + + return result diff --git a/app/tool/ask_human.py b/app/tool/ask_human.py index 5fd455070..dc42bee94 100644 --- a/app/tool/ask_human.py +++ b/app/tool/ask_human.py @@ -1,11 +1,15 @@ from app.tool import BaseTool +from app.tool.user_input_tool import WaitForUserInput class AskHuman(BaseTool): """Add a tool to ask human for help.""" name: str = "ask_human" - description: str = "Use this tool to ask human for help." + description: str = ( + "Legacy alias for optional mid-task user input. Do not use this to block task " + "progress or ask for clarification; continue autonomously if no reply arrives." + ) parameters: str = { "type": "object", "properties": { @@ -18,4 +22,5 @@ class AskHuman(BaseTool): } async def execute(self, inquire: str) -> str: - return input(f"""Bot: {inquire}\n\nYou: """).strip() + result = await WaitForUserInput().execute(message=inquire, timeout_seconds=30) + return str(result) diff --git a/app/tool/base.py b/app/tool/base.py index fdb8b7d3a..105ada42e 100644 --- a/app/tool/base.py +++ b/app/tool/base.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from typing import Any, Dict, Optional, Union -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from app.utils.logger import logger @@ -36,15 +36,34 @@ class ToolResult(BaseModel): - """Represents the result of a tool execution.""" + """Represents the result of a tool execution. + + Fields + ------ + output : Primary text output from the tool. + error : Error message when the tool failed, or None on success. + base64_image: Optional screenshot / image attached to the result. + system : Optional system-level note (e.g. sandbox info). + exit_code : Numeric exit status (0 = success, non-zero = failure). + metadata : Arbitrary key-value context (e.g. path, lines_changed, url). + """ output: Any = Field(default=None) error: Optional[str] = Field(default=None) base64_image: Optional[str] = Field(default=None) system: Optional[str] = Field(default=None) + exit_code: int = Field(default=0, description="Exit status; 0 = success") + metadata: Dict[str, Any] = Field( + default_factory=dict, + description="Structured context attached to the result (path, diff, url, …)", + ) + + model_config = ConfigDict(arbitrary_types_allowed=True) - class Config: - arbitrary_types_allowed = True + @property + def is_error(self) -> bool: + """True when the tool reported a failure.""" + return bool(self.error) or self.exit_code != 0 def __bool__(self): return any(getattr(self, field) for field in self.__fields__) @@ -59,20 +78,22 @@ def combine_fields( raise ValueError("Cannot combine tool results") return field or other_field + combined_meta = {**self.metadata, **other.metadata} return ToolResult( output=combine_fields(self.output, other.output), error=combine_fields(self.error, other.error), base64_image=combine_fields(self.base64_image, other.base64_image, False), system=combine_fields(self.system, other.system), + exit_code=other.exit_code if other.exit_code != 0 else self.exit_code, + metadata=combined_meta, ) def __str__(self): - return f"Error: {self.error}" if self.error else self.output + return f"Error: {self.error}" if self.error else str(self.output or "") def replace(self, **kwargs): """Returns a new ToolResult with the given fields replaced.""" - # return self.copy(update=kwargs) - return type(self)(**{**self.dict(), **kwargs}) + return type(self)(**{**self.model_dump(), **kwargs}) class BaseTool(ABC, BaseModel): @@ -84,21 +105,34 @@ class BaseTool(ABC, BaseModel): - Standardized result handling - Abstract execution interface - Attributes: - name (str): Tool name - description (str): Tool description - parameters (dict): Tool parameters schema - _schemas (Dict[str, List[ToolSchema]]): Registered method schemas + Capability flags + ---------------- + parallel_safe : Tool can run concurrently with other tools in the same step + (no shared mutable state, no exclusive resource locks). + can_retry : A failed call may be retried once with the error injected as + context. Set to False for destructive / non-idempotent tools. + emits_progress : Tool streams intermediate progress events during execution. """ name: str description: str parameters: Optional[dict] = None - # _schemas: Dict[str, List[ToolSchema]] = {} - class Config: - arbitrary_types_allowed = True - underscore_attrs_are_private = False + # --- Capability flags (read by ToolCallAgent) --- + parallel_safe: bool = Field( + default=False, + description="Safe to run in parallel with other tool calls in the same step.", + ) + can_retry: bool = Field( + default=True, + description="Whether a single retry-with-error-feedback is allowed on failure.", + ) + emits_progress: bool = Field( + default=False, + description="Tool streams intermediate progress events (e.g. long bash commands).", + ) + + model_config = ConfigDict(arbitrary_types_allowed=True, protected_namespaces=()) # def __init__(self, **data): # """Initialize tool with model validation and schema registration.""" diff --git a/app/tool/bash.py b/app/tool/bash.py index c6b9072fd..10624da2a 100644 --- a/app/tool/bash.py +++ b/app/tool/bash.py @@ -3,6 +3,12 @@ from typing import Optional from app.exceptions import ToolError +from app.task_context import ( + emit_current_task, + get_current_sandbox, + get_current_task, + get_current_tool_call, +) from app.tool.base import BaseTool, CLIResult @@ -79,6 +85,9 @@ async def run(self, command: str): # read output from the process, until the sentinel is found try: + last_output_len = 0 + last_error_len = 0 + tool_call = get_current_tool_call() or {} async with asyncio.timeout(self._timeout): while True: await asyncio.sleep(self._output_delay) @@ -87,6 +96,38 @@ async def run(self, command: str): output = ( self._process.stdout._buffer.decode() ) # pyright: ignore[reportAttributeAccessIssue] + visible_output = ( + output[: output.index(self._sentinel)] + if self._sentinel in output + else output + ) + if len(visible_output) > last_output_len: + emit_current_task( + "terminal_output", + { + "id": tool_call.get("id"), + "name": tool_call.get("name", "bash"), + "stream": "stdout", + "chunk": visible_output[last_output_len:], + }, + ) + last_output_len = len(visible_output) + + error = ( + self._process.stderr._buffer.decode() + ) # pyright: ignore[reportAttributeAccessIssue] + if len(error) > last_error_len: + emit_current_task( + "terminal_output", + { + "id": tool_call.get("id"), + "name": tool_call.get("name", "bash"), + "stream": "stderr", + "chunk": error[last_error_len:], + }, + ) + last_error_len = len(error) + if self._sentinel in output: # strip the sentinel and break output = output[: output.index(self._sentinel)] @@ -118,6 +159,7 @@ class Bash(BaseTool): name: str = "bash" description: str = _BASH_DESCRIPTION + emits_progress: bool = True # streams stdout/stderr chunks while running parameters: dict = { "type": "object", "properties": { @@ -134,6 +176,41 @@ class Bash(BaseTool): async def execute( self, command: str | None = None, restart: bool = False, **kwargs ) -> CLIResult: + sandbox = get_current_sandbox() + if sandbox is not None: + if not command: + raise ToolError("no command provided.") + tool_call = get_current_tool_call() or {} + task = get_current_task() + + # Emit start heartbeat + emit_current_task( + "tool:progress", + { + "id": tool_call.get("id"), + "name": self.name, + "status": "running", + "message": f"Executing: {command[:120]}", + }, + ) + code, stdout, stderr = await sandbox.run( + command, + timeout=kwargs.get("timeout") or 120, + task=task, + tool_call=tool_call, + tool_name=self.name, + ) + emit_current_task( + "tool:progress", + { + "id": tool_call.get("id"), + "name": self.name, + "status": "done", + "exit_code": code, + }, + ) + return CLIResult(output=stdout, error=stderr if code else None) + if restart: if self._session: self._session.stop() diff --git a/app/tool/browser_use_tool.py b/app/tool/browser_use_tool.py index 449e8e558..eecd5a3d3 100644 --- a/app/tool/browser_use_tool.py +++ b/app/tool/browser_use_tool.py @@ -12,10 +12,35 @@ from app.config import config from app.llm import LLM +from app.logger import logger from app.tool.base import BaseTool, ToolResult from app.tool.web_search import WebSearch +def _get_cloak_binary_path() -> Optional[str]: + """Return the CloakBrowser stealth Chromium binary path if available. + + Downloads the binary on first call (no-op if already cached). + Returns None on any error so the caller falls back to stock Playwright. + """ + try: + import cloakbrowser + + info = cloakbrowser.binary_info() + if info.get("installed"): + return info["binary_path"] + # Binary not yet cached — download it (happens once, stored in ~/.cloakbrowser/) + logger.info("CloakBrowser: binary not cached, downloading now…") + path = cloakbrowser.ensure_binary() + logger.info(f"CloakBrowser: stealth binary ready at {path}") + return path + except Exception as exc: + logger.warning( + f"CloakBrowser unavailable, falling back to stock Playwright: {exc}" + ) + return None + + _BROWSER_DESCRIPTION = """\ A powerful browser automation tool that allows interaction with web pages through various actions. * This tool provides commands for controlling a browser session, navigating web pages, and extracting information @@ -139,9 +164,14 @@ def validate_parameters(cls, v: dict, info: ValidationInfo) -> dict: return v async def _ensure_browser_initialized(self) -> BrowserContext: - """Ensure browser and context are initialized.""" + """Ensure browser and context are initialized. + + Prefers CloakBrowser's stealth Chromium binary (source-level fingerprint + patches, passes 30/30 bot detection tests) over stock Playwright Chromium. + Falls back to stock Playwright if CloakBrowser is unavailable. + """ if self.browser is None: - browser_config_kwargs = {"headless": False, "disable_security": True} + browser_config_kwargs: dict = {"headless": True, "disable_security": True} if config.browser_config: from browser_use.browser.browser import ProxySettings @@ -169,6 +199,23 @@ async def _ensure_browser_initialized(self) -> BrowserContext: if not isinstance(value, list) or value: browser_config_kwargs[attr] = value + # Inject CloakBrowser stealth binary unless the caller already + # specified a custom chrome_instance_path / wss_url / cdp_url. + if not any( + k in browser_config_kwargs + for k in ("chrome_instance_path", "wss_url", "cdp_url") + ): + cloak_path = _get_cloak_binary_path() + if cloak_path: + browser_config_kwargs["chrome_instance_path"] = cloak_path + logger.info( + f"BrowserUseTool: using CloakBrowser stealth binary → {cloak_path}" + ) + else: + logger.info( + "BrowserUseTool: CloakBrowser not available, using stock Playwright Chromium" + ) + self.browser = BrowserUseBrowser(BrowserConfig(**browser_config_kwargs)) if self.context is None: @@ -255,8 +302,14 @@ async def execute( ) # Execute the web search and return results directly without browser navigation search_response = await self.web_search_tool.execute( - query=query, fetch_content=True, num_results=1 + query=query, fetch_content=True, num_results=3 ) + # If search failed or returned no results, return the error directly + if search_response.error or not search_response.results: + return ToolResult( + error=search_response.error + or f"No search results found for query: {query}" + ) # Navigate to the first search result first_search_result = search_response.results[0] url_to_navigate = first_search_result.url @@ -379,9 +432,30 @@ async def execute( ) page = await context.get_current_page() - import markdownify - content = markdownify.markdownify(await page.content()) + # Prefer innerText — works on SPAs and JS-rendered pages. + # Falls back to markdownify on the raw HTML for static pages. + try: + content = await page.inner_text("body") + content = content.strip() + except Exception: + content = "" + + if not content: + import markdownify + + content = markdownify.markdownify(await page.content()).strip() + + if not content: + current_url = page.url + return ToolResult( + error=( + f"Page at {current_url} returned no readable content. " + "The page may require authentication, be rate-limiting the browser, " + "or be a JavaScript SPA that has not yet rendered. " + "Try: scroll down first, wait a few seconds, or navigate to a different URL." + ) + ) prompt = f"""\ Your task is to extract the content of the page. You will be given a page and a goal, and you should extract all relevant information around this goal from the page. If the goal is vague, summarize the page. Respond in json format. diff --git a/app/tool/codebase.py b/app/tool/codebase.py new file mode 100644 index 000000000..e9c102726 --- /dev/null +++ b/app/tool/codebase.py @@ -0,0 +1,390 @@ +"""Codebase navigation tools for fast, low-friction agent inspection.""" + +from __future__ import annotations + +import fnmatch +import os +import shutil +import subprocess +from pathlib import Path +from typing import Iterable, Optional + +from app.task_context import get_current_workspace +from app.tool.base import BaseTool, ToolResult + + +MAX_OUTPUT_CHARS = 24000 +DEFAULT_IGNORES = { + ".git", + ".hg", + ".svn", + "__pycache__", + ".pytest_cache", + ".mypy_cache", + ".ruff_cache", + ".venv", + "venv", + "node_modules", + "dist", + "build", + ".next", + ".turbo", + "coverage", +} + + +def _resolve_path(path: Optional[str] = None) -> Path: + workspace = get_current_workspace() + if path and workspace and (path == "/workspace" or path.startswith("/workspace/")): + rel = path.removeprefix("/workspace").lstrip("/") + return (Path(workspace) / rel).resolve() + base = Path.cwd() + if not path: + return base + candidate = Path(path).expanduser() + return candidate if candidate.is_absolute() else (base / candidate).resolve() + + +def _truncate(text: str, max_chars: int = MAX_OUTPUT_CHARS) -> str: + if len(text) <= max_chars: + return text + return ( + text[:max_chars] + + "\n\n" + ) + + +def _walk_files(root: Path) -> Iterable[Path]: + for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = [ + name + for name in dirnames + if name not in DEFAULT_IGNORES and not name.startswith(".") + ] + for filename in filenames: + if filename.startswith("."): + continue + yield Path(dirpath) / filename + + +class GlobSearch(BaseTool): + """Find files by glob pattern.""" + + name: str = "glob" + parallel_safe: bool = True # read-only, no shared state + description: str = ( + "Fast file discovery by glob pattern. Use this before reading files when " + "you need to locate likely implementation, config, test, or artifact files." + ) + parameters: dict = { + "type": "object", + "properties": { + "pattern": { + "type": "string", + "description": "Glob pattern, e.g. '**/*.py', 'frontend/src/**/*.tsx', '*.log'.", + }, + "path": { + "type": "string", + "description": "Optional directory to search. Defaults to the current conversation workspace.", + }, + "max_results": { + "type": "integer", + "description": "Maximum number of paths to return. Defaults to 200.", + }, + }, + "required": ["pattern"], + } + + async def execute( + self, pattern: str, path: Optional[str] = None, max_results: int = 200, **kwargs + ) -> ToolResult: + root = _resolve_path(path) + if not root.exists(): + return ToolResult(error=f"Path does not exist: {root}") + + limit = max(1, min(max_results or 200, 1000)) + results: list[str] = [] + for file_path in _walk_files(root): + rel = file_path.relative_to(root) + if fnmatch.fnmatch(str(rel), pattern) or fnmatch.fnmatch( + file_path.name, pattern + ): + results.append(str(rel)) + if len(results) >= limit: + break + + if not results: + return ToolResult(output=f"No files matched {pattern!r} under {root}") + + suffix = "\n" if len(results) >= limit else "" + return ToolResult(output="\n".join(results) + suffix) + + +class GrepSearch(BaseTool): + """Search file contents with ripgrep when available.""" + + name: str = "grep" + parallel_safe: bool = True # read-only, no shared state + description: str = ( + "Fast text search with line numbers. Use this to locate symbols, errors, TODOs, " + "or relevant code before opening files." + ) + parameters: dict = { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Text or regex to search for."}, + "path": { + "type": "string", + "description": "Optional file or directory. Defaults to the current conversation workspace.", + }, + "glob": { + "type": "string", + "description": "Optional file glob filter, e.g. '*.py' or 'src/**/*.tsx'.", + }, + "case_sensitive": { + "type": "boolean", + "description": "Whether matching is case-sensitive. Defaults to false.", + }, + "context_lines": { + "type": "integer", + "description": "Number of context lines around each match. Defaults to 0.", + }, + "max_results": { + "type": "integer", + "description": "Maximum matching lines to return. Defaults to 200.", + }, + }, + "required": ["query"], + } + + async def execute( + self, + query: str, + path: Optional[str] = None, + glob: Optional[str] = None, + case_sensitive: bool = False, + context_lines: int = 0, + max_results: int = 200, + **kwargs, + ) -> ToolResult: + target = _resolve_path(path) + if not target.exists(): + return ToolResult(error=f"Path does not exist: {target}") + + limit = max(1, min(max_results or 200, 1000)) + rg = shutil.which("rg") + if rg: + cmd = [ + rg, + "--line-number", + "--no-heading", + "--color=never", + "--max-count", + str(limit), + ] + if not case_sensitive: + cmd.append("--ignore-case") + if context_lines: + cmd.extend(["--context", str(max(0, min(context_lines, 10)))]) + if glob: + cmd.extend(["--glob", glob]) + cmd.extend([query, str(target)]) + proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if proc.returncode == 1: + return ToolResult(output=f"No matches for {query!r} in {target}") + if proc.returncode != 0: + return ToolResult(error=proc.stderr.strip() or "ripgrep failed") + return ToolResult(output=_truncate(proc.stdout)) + + matches: list[str] = [] + needle = query if case_sensitive else query.lower() + for file_path in _walk_files(target if target.is_dir() else target.parent): + if target.is_file() and file_path != target: + continue + if glob and not fnmatch.fnmatch(str(file_path), glob): + continue + try: + lines = file_path.read_text(errors="replace").splitlines() + except OSError: + continue + for index, line in enumerate(lines, start=1): + haystack = line if case_sensitive else line.lower() + if needle in haystack: + matches.append(f"{file_path}:{index}:{line}") + if len(matches) >= limit: + return ToolResult(output=_truncate("\n".join(matches))) + + if not matches: + return ToolResult(output=f"No matches for {query!r} in {target}") + return ToolResult(output=_truncate("\n".join(matches))) + + +class ReadFiles(BaseTool): + """Read one or more files with line numbers.""" + + name: str = "read_files" + parallel_safe: bool = True # read-only, no shared state + description: str = ( + "Read one or more files with line numbers. Prefer this for small batches of " + "related files after glob/grep has found them." + ) + parameters: dict = { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Single file path to read (convenience alias for paths=[path]).", + }, + "paths": { + "type": "array", + "items": {"type": "string"}, + "description": "File paths to read. Relative paths resolve from the current conversation workspace.", + }, + "start_line": { + "type": "integer", + "description": "Optional 1-based start line.", + }, + "end_line": { + "type": "integer", + "description": "Optional inclusive end line.", + }, + "max_chars_per_file": { + "type": "integer", + "description": "Maximum characters per file. Defaults to 12000.", + }, + }, + "anyOf": [ + {"required": ["paths"]}, + {"required": ["path"]}, + ], + } + + async def execute( + self, + paths: Optional[list[str]] = None, + path: Optional[str] = None, + start_line: Optional[int] = None, + end_line: Optional[int] = None, + max_chars_per_file: int = 12000, + **kwargs, + ) -> ToolResult: + if paths is None and path: + paths = [path] + elif path and paths is not None: + paths = [path, *paths] + + if not paths: + return ToolResult( + error="At least one path is required via 'path' or 'paths'" + ) + + limit = max(1000, min(max_chars_per_file or 12000, 50000)) + sections: list[str] = [] + for raw_path in paths[:20]: + file_path = _resolve_path(raw_path) + if not file_path.exists(): + sections.append(f"== {raw_path} ==\nERROR: file does not exist") + continue + if not file_path.is_file(): + sections.append(f"== {raw_path} ==\nERROR: not a file") + continue + + try: + lines = file_path.read_text(errors="replace").splitlines() + except OSError as exc: + sections.append(f"== {raw_path} ==\nERROR: {exc}") + continue + + start = max(1, start_line or 1) + end = min(len(lines), end_line or len(lines)) + numbered = [ + f"{line_no:>5}\t{lines[line_no - 1]}" + for line_no in range(start, end + 1) + ] + content = "\n".join(numbered) + sections.append(f"== {file_path} ==\n{_truncate(content, limit)}") + + return ToolResult(output=_truncate("\n\n".join(sections))) + + +class CodebaseOverview(BaseTool): + """Summarize workspace structure and likely verification commands.""" + + name: str = "codebase_overview" + parallel_safe: bool = True # read-only, no shared state + description: str = ( + "Inspect the current workspace quickly: important files, detected languages, " + "package managers, and likely build/test commands." + ) + parameters: dict = { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Optional directory. Defaults to the current conversation workspace.", + }, + "max_files": { + "type": "integer", + "description": "Maximum files to include in the tree. Defaults to 120.", + }, + }, + } + + async def execute( + self, path: Optional[str] = None, max_files: int = 120, **kwargs + ) -> ToolResult: + root = _resolve_path(path) + if not root.exists(): + return ToolResult(error=f"Path does not exist: {root}") + + limit = max(20, min(max_files or 120, 500)) + files = sorted(_walk_files(root), key=lambda p: str(p.relative_to(root)))[ + :limit + ] + rel_files = [str(file_path.relative_to(root)) for file_path in files] + names = {Path(p).name for p in rel_files} + suffixes = {Path(p).suffix for p in rel_files} + + commands: list[str] = [] + if "package.json" in names: + commands.extend(["npm run build", "npm test"]) + if ( + "pyproject.toml" in names + or "pytest.ini" in names + or "requirements.txt" in names + ): + commands.append("pytest -q") + if "Cargo.toml" in names: + commands.extend(["cargo test", "cargo clippy"]) + if "go.mod" in names: + commands.append("go test ./...") + if "pom.xml" in names: + commands.append("mvn test") + if "build.gradle" in names or "build.gradle.kts" in names: + commands.append("./gradlew test") + + language_hints = [] + if ".py" in suffixes: + language_hints.append("Python") + if suffixes & {".ts", ".tsx", ".js", ".jsx"}: + language_hints.append("JavaScript/TypeScript") + if ".rs" in suffixes: + language_hints.append("Rust") + if ".go" in suffixes: + language_hints.append("Go") + if ".java" in suffixes: + language_hints.append("Java") + if ".tex" in suffixes: + language_hints.append("LaTeX") + + output = [ + f"Workspace: {root}", + f"Detected languages: {', '.join(language_hints) or 'unknown'}", + "Likely verification commands:", + *(f"- {cmd}" for cmd in commands), + "Files:", + *(f"- {file}" for file in rel_files), + ] + if len(files) >= limit: + output.append("") + return ToolResult(output="\n".join(output)) diff --git a/app/tool/file_operators.py b/app/tool/file_operators.py index dd64c8386..5be3e869f 100644 --- a/app/tool/file_operators.py +++ b/app/tool/file_operators.py @@ -7,6 +7,7 @@ from app.config import SandboxSettings from app.exceptions import ToolError from app.sandbox.client import SANDBOX_CLIENT +from app.task_context import get_current_sandbox PathLike = Union[str, Path] @@ -101,6 +102,8 @@ def __init__(self): async def _ensure_sandbox_initialized(self): """Ensure sandbox is initialized.""" + if get_current_sandbox() is not None: + return if not self.sandbox_client.sandbox: await self.sandbox_client.create(config=SandboxSettings()) @@ -108,6 +111,9 @@ async def read_file(self, path: PathLike) -> str: """Read content from a file in sandbox.""" await self._ensure_sandbox_initialized() try: + current_sandbox = get_current_sandbox() + if current_sandbox is not None: + return await current_sandbox.read_file(str(path)) return await self.sandbox_client.read_file(str(path)) except Exception as e: raise ToolError(f"Failed to read {path} in sandbox: {str(e)}") from None @@ -116,6 +122,10 @@ async def write_file(self, path: PathLike, content: str) -> None: """Write content to a file in sandbox.""" await self._ensure_sandbox_initialized() try: + current_sandbox = get_current_sandbox() + if current_sandbox is not None: + await current_sandbox.write_file(str(path), content) + return await self.sandbox_client.write_file(str(path), content) except Exception as e: raise ToolError(f"Failed to write to {path} in sandbox: {str(e)}") from None @@ -123,6 +133,9 @@ async def write_file(self, path: PathLike, content: str) -> None: async def is_directory(self, path: PathLike) -> bool: """Check if path points to a directory in sandbox.""" await self._ensure_sandbox_initialized() + current_sandbox = get_current_sandbox() + if current_sandbox is not None: + return await current_sandbox.is_directory(str(path)) result = await self.sandbox_client.run_command( f"test -d {path} && echo 'true' || echo 'false'" ) @@ -131,6 +144,9 @@ async def is_directory(self, path: PathLike) -> bool: async def exists(self, path: PathLike) -> bool: """Check if path exists in sandbox.""" await self._ensure_sandbox_initialized() + current_sandbox = get_current_sandbox() + if current_sandbox is not None: + return await current_sandbox.exists(str(path)) result = await self.sandbox_client.run_command( f"test -e {path} && echo 'true' || echo 'false'" ) @@ -142,6 +158,12 @@ async def run_command( """Run a command in sandbox environment.""" await self._ensure_sandbox_initialized() try: + current_sandbox = get_current_sandbox() + if current_sandbox is not None: + return_code, stdout, stderr = await current_sandbox.run( + cmd, timeout=int(timeout) if timeout else None + ) + return return_code, stdout, stderr stdout = await self.sandbox_client.run_command( cmd, timeout=int(timeout) if timeout else None ) diff --git a/app/tool/line_edit.py b/app/tool/line_edit.py new file mode 100644 index 000000000..28f921029 --- /dev/null +++ b/app/tool/line_edit.py @@ -0,0 +1,192 @@ +""" +LineEdit — unambiguous line-number-based file editor. + +Why this exists: + str_replace_editor fails when the target string has any whitespace difference, + duplicate occurrence, or the model hallucinates context lines. + apply_patch_editor requires clean hunk headers and can reject drifted patches. + + This tool avoids string matching entirely. The model reads the file with + line numbers, identifies the exact range it wants to change, and passes + start_line / end_line directly. The replacement is applied by list slicing — + it CANNOT mis-match. + +Workflow the model should follow: + 1. Call str_replace_editor view OR bash "cat -n " to see line numbers. + 2. Identify start_line and end_line of the region to replace (inclusive, 1-indexed). + 3. Call line_edit with path, start_line, end_line, new_content. + 4. Verify the result snippet shown in the response. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from app.config import config +from app.exceptions import ToolError +from app.task_context import emit_current_task, get_current_tool_call +from app.tool.base import BaseTool +from app.tool.file_operators import LocalFileOperator, SandboxFileOperator + + +SNIPPET_CONTEXT = 4 # lines of context shown above/below the edit in the response + + +class LineEdit(BaseTool): + """Replace a range of lines in a file by line number — no string matching.""" + + name: str = "line_edit" + description: str = ( + "Edit a file by replacing a specific range of lines (start_line to end_line, " + "inclusive, 1-indexed) with new_content. " + "ALWAYS read the file with line numbers first (use str_replace_editor view or " + "'cat -n'), identify the exact line range, then call this tool. " + "This never fails due to string matching issues. " + "Use this as the primary method for all code edits — " + "only fall back to apply_patch_editor for multi-file atomic changes." + ) + parameters: dict = { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Absolute path to the file to edit.", + }, + "start_line": { + "type": "integer", + "description": ( + "First line of the region to replace (1-indexed, inclusive). " + "To insert BEFORE line N use start_line=N, end_line=N-1 (empty range). " + "To insert at end of file use start_line=, end_line=." + ), + }, + "end_line": { + "type": "integer", + "description": ( + "Last line of the region to replace (1-indexed, inclusive). " + "Use -1 to mean 'end of file'. " + "Set end_line < start_line to insert without deleting any lines." + ), + }, + "new_content": { + "type": "string", + "description": ( + "Replacement text for the selected line range. " + "Must include correct indentation. " + "Pass an empty string to delete the selected lines." + ), + }, + }, + "required": ["path", "start_line", "end_line", "new_content"], + } + + _local_operator: LocalFileOperator = LocalFileOperator() + _sandbox_operator: SandboxFileOperator = SandboxFileOperator() + + def _operator(self): + return ( + self._sandbox_operator + if config.sandbox.use_sandbox + else self._local_operator + ) + + async def execute( + self, + *, + path: str, + start_line: int, + end_line: int, + new_content: str, + **kwargs: Any, + ) -> str: + p = Path(path) + if not p.is_absolute(): + raise ToolError(f"path must be absolute, got: {path!r}") + + op = self._operator() + + if not await op.exists(path): + raise ToolError(f"File not found: {path}") + if await op.is_directory(path): + raise ToolError(f"path is a directory, not a file: {path}") + + original = await op.read_file(path) + lines = original.splitlines(keepends=True) + total = len(lines) + + # Normalise end_line=-1 → last line + if end_line == -1: + end_line = total + + # Convert to 0-based slice indices + # start_line=1, end_line=3 → lines[0:3] + slice_start = max(0, start_line - 1) + slice_end = end_line # end_line=3 means up to and including line 3 → index 3 + + if slice_start > total: + raise ToolError( + f"start_line {start_line} is beyond end of file ({total} lines)." + ) + if slice_end < slice_start: + # Pure insert: no lines deleted + slice_end = slice_start + + # Build replacement lines (ensure trailing newline on each line) + if new_content: + replacement_lines = [] + for ln in new_content.splitlines(): + replacement_lines.append(ln + "\n") + # Remove the trailing newline from the very last replacement line + # only if the original file didn't end with a blank line there — + # just keep it simple and always include it. + else: + replacement_lines = [] + + new_lines = lines[:slice_start] + replacement_lines + lines[slice_end:] + new_text = "".join(new_lines) + + await op.write_file(path, new_text) + + # Build a context snippet for the response + edit_start_0 = slice_start + edit_end_0 = slice_start + len(replacement_lines) + snip_start = max(0, edit_start_0 - SNIPPET_CONTEXT) + snip_end = min(len(new_lines), edit_end_0 + SNIPPET_CONTEXT) + + snippet_lines = new_lines[snip_start:snip_end] + snippet = "".join( + f"{snip_start + i + 1:6}\t{ln}" for i, ln in enumerate(snippet_lines) + ) + + lines_deleted = max(0, slice_end - slice_start) + lines_added = len(replacement_lines) + + summary = ( + f"Edited {path}: replaced lines {start_line}–{end_line} " + f"(-{lines_deleted} +{lines_added} lines).\n" + f"File now has {len(new_lines)} lines.\n\n" + f"Context around edit (lines {snip_start + 1}–{snip_end}):\n" + f"{snippet}" + ) + + # Emit workspace_file_updated event for the UI diff panel + tool_call = get_current_tool_call() or {} + emit_current_task( + "workspace_file_updated", + { + "tool_call_id": str(tool_call.get("id") or ""), + "tool": self.name, + "path": path, + "added_lines": lines_added, + "deleted_lines": lines_deleted, + "diff_preview": { + "command": "line_edit", + "start_line": start_line, + "end_line": end_line, + "lines": snippet_lines[:120], + }, + }, + ) + + return summary diff --git a/app/tool/long_term_memory.py b/app/tool/long_term_memory.py new file mode 100644 index 000000000..ca5c824ad --- /dev/null +++ b/app/tool/long_term_memory.py @@ -0,0 +1,88 @@ +from pathlib import Path +from typing import Optional + +from app.memory.agentmemory import agentmemory +from app.task_context import get_current_workspace +from app.tool.base import BaseTool, ToolResult + + +def _conversation_id_from_workspace() -> str: + workspace = get_current_workspace() or "" + if not workspace: + return "default" + path = Path(workspace) + parts = path.parts + if "conversations" in parts: + idx = parts.index("conversations") + if idx + 1 < len(parts): + return parts[idx + 1] + return path.name or "default" + + +class MemorySave(BaseTool): + name: str = "memory_save" + description: str = ( + "Save durable long-term memory for this conversation using AgentMemory. " + "Use for decisions, constraints, discoveries, and key outputs." + ) + parameters: dict = { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Memory content to store.", + }, + "title": { + "type": "string", + "description": "Short title for the memory.", + }, + }, + "required": ["content"], + } + + async def execute(self, content: str, title: Optional[str] = None) -> ToolResult: + if not agentmemory.enabled: + return ToolResult( + output="AgentMemory is disabled in config; skipping memory save." + ) + conversation_id = _conversation_id_from_workspace() + saved = agentmemory.remember( + conversation_id=conversation_id, + title=title or "Conversation memory", + content=content, + ) + if not saved: + return ToolResult(error="Failed to save memory to AgentMemory.") + return ToolResult(output="Memory saved.") + + +class MemoryRecall(BaseTool): + name: str = "memory_recall" + description: str = "Recall relevant long-term memory snippets from AgentMemory for this conversation." + parameters: dict = { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "What to recall.", + }, + "limit": { + "type": "integer", + "description": "Optional max results.", + }, + }, + "required": ["query"], + } + + async def execute(self, query: str, limit: Optional[int] = None) -> ToolResult: + if not agentmemory.enabled: + return ToolResult( + output="AgentMemory is disabled in config; no long-term recall available." + ) + conversation_id = _conversation_id_from_workspace() + context = agentmemory.format_context( + conversation_id=conversation_id, query=query, limit=limit + ) + if not context: + return ToolResult(output="No matching long-term memories found.") + return ToolResult(output=context) diff --git a/app/tool/planning.py b/app/tool/planning.py index 47e334d66..bffc034d3 100644 --- a/app/tool/planning.py +++ b/app/tool/planning.py @@ -2,6 +2,7 @@ from typing import Dict, List, Literal, Optional from app.exceptions import ToolError +from app.task_context import emit_current_task from app.tool.base import BaseTool, ToolResult @@ -153,9 +154,11 @@ def _create_plan( self.plans[plan_id] = plan self._current_plan_id = plan_id # Set as active plan - return ToolResult( + result = ToolResult( output=f"Plan created successfully with ID: {plan_id}\n\n{self._format_plan(plan)}" ) + self._emit_plan_event(plan, command="create") + return result def _update_plan( self, plan_id: Optional[str], title: Optional[str], steps: Optional[List[str]] @@ -202,9 +205,11 @@ def _update_plan( plan["step_statuses"] = new_statuses plan["step_notes"] = new_notes - return ToolResult( + result = ToolResult( output=f"Plan updated successfully: {plan_id}\n\n{self._format_plan(plan)}" ) + self._emit_plan_event(plan, command="update") + return result def _list_plans(self) -> ToolResult: """List all available plans.""" @@ -299,9 +304,11 @@ def _mark_step( if step_notes: plan["step_notes"][step_index] = step_notes - return ToolResult( + result = ToolResult( output=f"Step {step_index} updated in plan '{plan_id}'.\n\n{self._format_plan(plan)}" ) + self._emit_plan_event(plan, command="mark_step", step_index=step_index) + return result def _delete_plan(self, plan_id: Optional[str]) -> ToolResult: """Delete a plan.""" @@ -317,7 +324,12 @@ def _delete_plan(self, plan_id: Optional[str]) -> ToolResult: if self._current_plan_id == plan_id: self._current_plan_id = None - return ToolResult(output=f"Plan '{plan_id}' has been deleted.") + result = ToolResult(output=f"Plan '{plan_id}' has been deleted.") + emit_current_task( + "agent:plan:updated", + {"command": "delete", "plan_id": plan_id, "deleted": True}, + ) + return result def _format_plan(self, plan: Dict) -> str: """Format a plan for display.""" @@ -361,3 +373,33 @@ def _format_plan(self, plan: Dict) -> str: output += f" Notes: {notes}\n" return output + + def _emit_plan_event( + self, plan: Dict, command: str, step_index: Optional[int] = None + ) -> None: + """Emit a structured plan state event for the UI to render.""" + total = len(plan["steps"]) + completed = sum(1 for s in plan["step_statuses"] if s == "completed") + emit_current_task( + "agent:plan:updated", + { + "command": command, + "plan_id": plan["plan_id"], + "title": plan["title"], + "steps": [ + { + "index": i, + "text": step, + "status": plan["step_statuses"][i], + "notes": plan["step_notes"][i], + "active": step_index == i, + } + for i, step in enumerate(plan["steps"]) + ], + "progress": { + "completed": completed, + "total": total, + "pct": round(completed / total * 100, 1) if total else 0, + }, + }, + ) diff --git a/app/tool/python_execute.py b/app/tool/python_execute.py index 08ceffa85..668fcb102 100644 --- a/app/tool/python_execute.py +++ b/app/tool/python_execute.py @@ -1,16 +1,27 @@ -import multiprocessing +import asyncio +import os import sys -from io import StringIO +import tempfile from typing import Dict +from app.task_context import ( + emit_current_task, + get_current_sandbox, + get_current_task, + get_current_tool_call, +) from app.tool.base import BaseTool class PythonExecute(BaseTool): - """A tool for executing Python code with timeout and safety restrictions.""" + """A tool for executing Python code with timeout and visible output.""" name: str = "python_execute" - description: str = "Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results." + description: str = ( + "Executes Python code string. Note: Only printed stdout/stderr are visible, " + "function return values are not captured. Use print statements to see results." + ) + emits_progress: bool = True # streams stdout/stderr lines while running parameters: dict = { "type": "object", "properties": { @@ -22,20 +33,6 @@ class PythonExecute(BaseTool): "required": ["code"], } - def _run_code(self, code: str, result_dict: dict, safe_globals: dict) -> None: - original_stdout = sys.stdout - try: - output_buffer = StringIO() - sys.stdout = output_buffer - exec(code, safe_globals, safe_globals) - result_dict["observation"] = output_buffer.getvalue() - result_dict["success"] = True - except Exception as e: - result_dict["observation"] = str(e) - result_dict["success"] = False - finally: - sys.stdout = original_stdout - async def execute( self, code: str, @@ -49,27 +46,113 @@ async def execute( timeout (int): Execution timeout in seconds. Returns: - Dict: Contains 'output' with execution output or error message and 'success' status. + Dict: Contains 'observation' with execution output and success status. """ + tool_call = get_current_tool_call() or {} + sandbox = get_current_sandbox() + if sandbox is not None: + script_path = "/workspace/.openmanus/python_execute.py" + await sandbox.write_file(script_path, code) + emit_current_task( + "tool:progress", + { + "id": tool_call.get("id"), + "name": self.name, + "status": "running", + "message": "Executing Python script in sandbox...", + }, + ) + return_code, stdout, stderr = await sandbox.run( + f"python -u {script_path}", + timeout=timeout, + task=get_current_task(), + tool_call=tool_call, + tool_name=self.name, + ) + emit_current_task( + "tool:progress", + { + "id": tool_call.get("id"), + "name": self.name, + "status": "done", + "exit_code": return_code, + }, + ) + return { + "observation": stdout + stderr, + "success": return_code == 0, + } + + fd, path = tempfile.mkstemp(prefix="openmanus_python_", suffix=".py") + try: + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write(code) - with multiprocessing.Manager() as manager: - result = manager.dict({"observation": "", "success": False}) - if isinstance(__builtins__, dict): - safe_globals = {"__builtins__": __builtins__} - else: - safe_globals = {"__builtins__": __builtins__.__dict__.copy()} - proc = multiprocessing.Process( - target=self._run_code, args=(code, result, safe_globals) + process = await asyncio.create_subprocess_exec( + sys.executable, + "-u", + path, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, ) - proc.start() - proc.join(timeout) - # timeout process - if proc.is_alive(): - proc.terminate() - proc.join(1) - return { - "observation": f"Execution timeout after {timeout} seconds", - "success": False, - } - return dict(result) + stdout_parts: list[str] = [] + stderr_parts: list[str] = [] + + async def read_stream(stream, stream_name: str, sink: list[str]) -> None: + while True: + chunk = await stream.readline() + if not chunk: + break + text = chunk.decode("utf-8", errors="replace") + sink.append(text) + emit_current_task( + "terminal_output", + { + "id": tool_call.get("id"), + "name": tool_call.get("name", self.name), + "stream": stream_name, + "chunk": text, + }, + ) + + stdout_task = asyncio.create_task( + read_stream(process.stdout, "stdout", stdout_parts) + ) + stderr_task = asyncio.create_task( + read_stream(process.stderr, "stderr", stderr_parts) + ) + + try: + await asyncio.wait_for(process.wait(), timeout=timeout) + except asyncio.TimeoutError: + process.kill() + await process.wait() + await asyncio.gather(stdout_task, stderr_task) + message = f"Execution timeout after {timeout} seconds" + emit_current_task( + "terminal_output", + { + "id": tool_call.get("id"), + "name": tool_call.get("name", self.name), + "stream": "stderr", + "chunk": message, + }, + ) + return {"observation": message, "success": False} + + await asyncio.gather(stdout_task, stderr_task) + stdout = "".join(stdout_parts) + stderr = "".join(stderr_parts) + observation = stdout + stderr + return { + "observation": observation, + "success": process.returncode == 0, + } + except Exception as exc: + return {"observation": str(exc), "success": False} + finally: + try: + os.remove(path) + except OSError: + pass diff --git a/app/tool/search/bing_search.py b/app/tool/search/bing_search.py index 620620cd3..1bb5eb374 100644 --- a/app/tool/search/bing_search.py +++ b/app/tool/search/bing_search.py @@ -1,7 +1,13 @@ -from typing import List, Optional, Tuple +import urllib.parse +from typing import List -import requests -from bs4 import BeautifulSoup + +try: + from cloakbrowser import launch as cloak_launch +except Exception: # pragma: no cover - optional dependency runtime probe + cloak_launch = None + +from playwright.sync_api import sync_playwright from app.logger import logger from app.tool.search.base import SearchItem, WebSearchEngine @@ -9,136 +15,135 @@ ABSTRACT_MAX_LENGTH = 300 -USER_AGENTS = [ - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36", - "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/49.0.2623.108 Chrome/49.0.2623.108 Safari/537.36", - "Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR) AppleWebKit/533.3 (KHTML, like Gecko) QtWeb Internet Browser/3.7 http://www.QtWeb.net", - "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", - "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) ChromePlus/4.0.222.3 Chrome/4.0.222.3 Safari/532.2", - "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.4pre) Gecko/20070404 K-Ninja/2.1.3", - "Mozilla/5.0 (Future Star Technologies Corp.; Star-Blade OS; x86_64; U; en-US) iNet Browser 4.7", - "Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:2.2) Gecko/20110201", - "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080414 Firefox/2.0.0.13 Pogo/2.0.0.13.6866", -] - -HEADERS = { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": USER_AGENTS[0], - "Referer": "https://www.bing.com/", - "Accept-Encoding": "gzip, deflate", - "Accept-Language": "zh-CN,zh;q=0.9", -} - -BING_HOST_URL = "https://www.bing.com" -BING_SEARCH_URL = "https://www.bing.com/search?q=" - class BingSearchEngine(WebSearchEngine): - session: Optional[requests.Session] = None - - def __init__(self, **data): - """Initialize the BingSearch tool with a requests session.""" - super().__init__(**data) - self.session = requests.Session() - self.session.headers.update(HEADERS) + def _search_with_cloakbrowser( + self, query: str, num_results: int + ) -> List[SearchItem]: + if cloak_launch is None: + return [] + list_result: List[SearchItem] = [] + browser = None + try: + browser = cloak_launch(humanize=True) + page = browser.new_page() + encoded_query = urllib.parse.quote(query) + # Force English results regardless of container IP geolocation + page.goto( + f"https://www.bing.com/search?q={encoded_query}&setlang=en&cc=US&mkt=en-US" + ) + try: + page.wait_for_selector("li.b_algo", timeout=8000) + except Exception: + pass + results = page.query_selector_all("li.b_algo") + for r in results: + if len(list_result) >= num_results: + break + try: + a = r.query_selector("h2 a") + desc = r.query_selector("p") + if not a: + continue + title = a.text_content().strip() if a.text_content() else "" + url = a.get_attribute("href") or "" + abstract = ( + desc.text_content().strip() + if desc and desc.text_content() + else "" + ) + if ABSTRACT_MAX_LENGTH and len(abstract) > ABSTRACT_MAX_LENGTH: + abstract = abstract[:ABSTRACT_MAX_LENGTH] + if title and url: + list_result.append( + SearchItem(title=title, url=url, description=abstract) + ) + except Exception: + continue + except Exception as e: + logger.warning(f"Error performing Bing CloakBrowser search: {e}") + return [] + finally: + if browser is not None: + try: + browser.close() + except Exception: + pass + return list_result - def _search_sync(self, query: str, num_results: int = 10) -> List[SearchItem]: + def perform_search( + self, query: str, num_results: int = 10, *args, **kwargs + ) -> List[SearchItem]: """ - Synchronous Bing search implementation to retrieve search results. - - Args: - query (str): The search query to submit to Bing. - num_results (int, optional): Maximum number of results to return. Defaults to 10. - - Returns: - List[SearchItem]: A list of search items with title, URL, and description. + Bing search engine using Playwright to bypass scraping blocks. """ if not query: return [] - list_result = [] - first = 1 - next_url = BING_SEARCH_URL + query - - while len(list_result) < num_results: - data, next_url = self._parse_html( - next_url, rank_start=len(list_result), first=first - ) - if data: - list_result.extend(data) - if not next_url: - break - first += 10 - - return list_result[:num_results] + # Prefer CloakBrowser when available, fallback to Playwright. + cloak_results = self._search_with_cloakbrowser(query, num_results) + if cloak_results: + return cloak_results - def _parse_html( - self, url: str, rank_start: int = 0, first: int = 1 - ) -> Tuple[List[SearchItem], str]: - """ - Parse Bing search result HTML to extract search results and the next page URL. - - Returns: - tuple: (List of SearchItem objects, next page URL or None) - """ + list_result: List[SearchItem] = [] try: - res = self.session.get(url=url) - res.encoding = "utf-8" - root = BeautifulSoup(res.text, "lxml") - - list_data = [] - ol_results = root.find("ol", id="b_results") - if not ol_results: - return [], None - - for li in ol_results.find_all("li", class_="b_algo"): - title = "" - url = "" - abstract = "" + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + # Create context with a realistic user agent + context = browser.new_context( + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + ) + page = context.new_page() + + # Load search page — force English results regardless of container IP + encoded_query = urllib.parse.quote(query) + page.goto( + f"https://www.bing.com/search?q={encoded_query}&setlang=en&cc=US&mkt=en-US" + ) + + # Wait for results or timeout try: - h2 = li.find("h2") - if h2: - title = h2.text.strip() - url = h2.a["href"].strip() + page.wait_for_selector("li.b_algo", timeout=8000) + except Exception: + pass # Continue even if no selector, maybe there are no results - p = li.find("p") - if p: - abstract = p.text.strip() + results = page.query_selector_all("li.b_algo") - if ABSTRACT_MAX_LENGTH and len(abstract) > ABSTRACT_MAX_LENGTH: - abstract = abstract[:ABSTRACT_MAX_LENGTH] + for r in results: + if len(list_result) >= num_results: + break - rank_start += 1 + try: + a = r.query_selector("h2 a") + desc = r.query_selector("p") - # Create a SearchItem object - list_data.append( - SearchItem( - title=title or f"Bing Result {rank_start}", - url=url, - description=abstract, - ) - ) - except Exception: - continue + if not a: + continue - next_btn = root.find("a", title="Next page") - if not next_btn: - return list_data, None + title = a.text_content().strip() if a.text_content() else "" + url = a.get_attribute("href") or "" + abstract = ( + desc.text_content().strip() + if desc and desc.text_content() + else "" + ) - next_url = BING_HOST_URL + next_btn["href"] - return list_data, next_url + if ABSTRACT_MAX_LENGTH and len(abstract) > ABSTRACT_MAX_LENGTH: + abstract = abstract[:ABSTRACT_MAX_LENGTH] + + if title and url: + list_result.append( + SearchItem( + title=title, + url=url, + description=abstract, + ) + ) + except Exception: + continue + + browser.close() except Exception as e: - logger.warning(f"Error parsing HTML: {e}") - return [], None + logger.warning(f"Error performing Bing Playwright search: {e}") - def perform_search( - self, query: str, num_results: int = 10, *args, **kwargs - ) -> List[SearchItem]: - """ - Bing search engine. - - Returns results formatted according to SearchItem model. - """ - return self._search_sync(query, num_results=num_results) + return list_result diff --git a/app/tool/search/yahoo_search.py b/app/tool/search/yahoo_search.py new file mode 100644 index 000000000..827689fc7 --- /dev/null +++ b/app/tool/search/yahoo_search.py @@ -0,0 +1,89 @@ +from typing import List, Optional + +import requests +from bs4 import BeautifulSoup + +from app.logger import logger +from app.tool.search.base import SearchItem, WebSearchEngine + + +ABSTRACT_MAX_LENGTH = 300 + +USER_AGENTS = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15", +] + +HEADERS = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "User-Agent": USER_AGENTS[0], + "Referer": "https://search.yahoo.com/", + "Accept-Language": "en-US,en;q=0.9", +} + + +class YahooSearchEngine(WebSearchEngine): + session: Optional[requests.Session] = None + + def __init__(self, **data): + super().__init__(**data) + self.session = requests.Session() + self.session.headers.update(HEADERS) + + def perform_search( + self, query: str, num_results: int = 10, *args, **kwargs + ) -> List[SearchItem]: + """Yahoo search engine.""" + if not query: + return [] + + list_result = [] + try: + res = self.session.get( + url=f"https://search.yahoo.com/search?p={query}", timeout=10 + ) + res.raise_for_status() + root = BeautifulSoup(res.text, "html.parser") + + for div in root.find_all("div", class_="compTitle"): + try: + title_elem = div.find("h3") + if not title_elem: + continue + + a_elem = title_elem.find("a") + if not a_elem: + continue + + title = a_elem.text.strip() + url = a_elem.get("href", "").strip() + + # Try to find the abstract by looking at the parent or next sibling + abstract = "" + parent_li = div.find_parent("li") + if parent_li: + desc_div = parent_li.find("div", class_="compText") + if desc_div: + abstract = desc_div.text.strip() + + if ABSTRACT_MAX_LENGTH and len(abstract) > ABSTRACT_MAX_LENGTH: + abstract = abstract[:ABSTRACT_MAX_LENGTH] + + if title and url: + list_result.append( + SearchItem( + title=title, + url=url, + description=abstract, + ) + ) + except Exception: + continue + + if len(list_result) >= num_results: + break + + return list_result + except Exception as e: + logger.warning(f"Error parsing Yahoo HTML: {e}") + return [] diff --git a/app/tool/skill_playbook.py b/app/tool/skill_playbook.py new file mode 100644 index 000000000..47fda0d60 --- /dev/null +++ b/app/tool/skill_playbook.py @@ -0,0 +1,135 @@ +"""Concise built-in workflow skills inspired by modern coding-agent harnesses.""" + +from __future__ import annotations + +from typing import Literal, Optional + +from app.skills import load_skills +from app.task_context import get_current_workspace +from app.tool.base import BaseTool, ToolResult + + +SkillName = Literal[ + "coding", + "debugging", + "frontend", + "testing", + "research", + "security", + "latex", + "data", + "git", +] + + +SKILLS: dict[str, str] = { + "coding": """Coding workflow: +1. Run codebase_overview, then glob/grep/read_files to locate the exact files. +2. Make the smallest coherent change that satisfies the request. +3. Prefer existing project patterns over new abstractions. +4. Run the narrowest meaningful verification command, then broader checks if shared behavior changed. +5. Inspect outputs/diffs before terminate.""", + "debugging": """Debugging workflow: +1. Reproduce or locate the failing symptom in logs/tests. +2. Trace from error message to owner code using grep and read_files. +3. Fix the cause, not only the visible error. +4. Re-run the failing command and inspect the new output. +5. If still failing, change strategy; do not repeat the same failed command blindly.""", + "frontend": """Frontend workflow: +1. Inspect existing components, styling utilities, and routing before changing UI. +2. Keep controls stable, scrollable, and responsive; avoid text overflow. +3. Use existing icons/components where available. +4. Build and, when possible, verify in browser with screenshots or visible output. +5. Report any remaining visual risk clearly.""", + "testing": """Testing workflow: +1. Discover project test commands from package/config files. +2. Run targeted tests first, then build/type checks. +3. Treat test environment failures separately from product failures. +4. If tests cannot run, capture the exact blocker and still run syntax/static checks where possible.""", + "research": """Research workflow: +1. Browse current primary sources for unstable or external facts. +2. Cross-check important claims with more than one reliable source. +3. Distill findings into decisions or implementation constraints. +4. Avoid copying large external text; adapt concepts to this codebase.""", + "security": """Security workflow: +1. Avoid exposing secrets, tokens, private keys, or local credentials. +2. Validate filesystem paths and user-controlled inputs. +3. Avoid destructive commands unless explicitly required. +4. Prefer least-privilege behavior and clear failure states. +5. Run available security/static checks when touching auth, shell, paths, or network code.""", + "latex": """LaTeX workflow: +1. Write .tex and assets into the current conversation workspace. +2. Compile with latexmk or pdflatex in nonstop mode. +3. Confirm a PDF exists before claiming success. +4. If no PDF exists, inspect the .log and fix/report the exact error.""", + "data": """Data workflow: +1. Inspect file schemas/samples before transforming. +2. Use Python for structured parsing instead of ad hoc shell when data is nontrivial. +3. Save outputs in the workspace with clear filenames. +4. Validate counts, columns, and sample rows after transformation.""", + "git": """Git workflow: +1. Inspect git status before edits if the task touches repository state. +2. Never revert unrelated user changes. +3. Keep diffs scoped and review them before finishing. +4. Do not commit or push unless explicitly asked.""", +} + + +class SkillPlaybook(BaseTool): + """Return concise workflow guidance for a specific task type.""" + + name: str = "skill_playbook" + description: str = ( + "Load a concise built-in or .openhands skill. Use this before specialized " + "coding, debugging, frontend, testing, research, security, latex, data, docker, " + "or git work. Pass list_available=true to see skills discovered in the current workspace." + ) + parameters: dict = { + "type": "object", + "properties": { + "skill": { + "type": "string", + "description": "Built-in skill name or discovered .openhands skill name.", + }, + "task": { + "type": "string", + "description": "Optional current task summary for context.", + }, + "list_available": { + "type": "boolean", + "description": "Return available built-in and .openhands skills instead of loading one.", + }, + }, + "required": [], + } + + async def execute( + self, + skill: Optional[str] = None, + task: Optional[str] = None, + list_available: bool = False, + **kwargs, + ) -> ToolResult: + workspace = get_current_workspace() + discovered = load_skills(workspace) + if list_available: + names = [f"- {name} (built-in)" for name in sorted(SKILLS.keys())] + names.extend( + f"- {item.name} ({item.type}; triggers: {', '.join(item.triggers or []) or 'always'})" + for item in discovered + ) + return ToolResult(output="Available skills:\n" + "\n".join(names)) + + if not skill: + return ToolResult(error="skill is required unless list_available=true") + + text = SKILLS.get(skill) + if not text: + found = next((item for item in discovered if item.name == skill), None) + if found is not None: + text = found.body + if not text: + return ToolResult(error=f"Unknown skill: {skill}. Try list_available=true.") + if task: + text = f"Task focus: {task}\n\n{text}" + return ToolResult(output=text) diff --git a/app/tool/str_replace_editor.py b/app/tool/str_replace_editor.py index a907f41e1..d6b06d5d0 100644 --- a/app/tool/str_replace_editor.py +++ b/app/tool/str_replace_editor.py @@ -136,6 +136,15 @@ async def execute( elif command == "create": if file_text is None: raise ToolError("Parameter `file_text` is required for command: create") + if await operator.exists(path): + return str( + ToolResult( + output=( + f"File already exists at: {path}. Skipped `create` (no changes). " + "Use `str_replace` or `insert` to edit existing files." + ) + ) + ) await operator.write_file(path, file_text) self._file_history[path].append(file_text) result = ToolResult(output=f"File created successfully at: {path}") @@ -185,13 +194,10 @@ async def validate_path( f"The path {path} is a directory and only the `view` command can be used on directories" ) - # Check if file exists for create command + # For create, existence is handled in execute() as a non-fatal no-op so + # long-running plans can continue idempotently. elif command == "create": - exists = await operator.exists(path) - if exists: - raise ToolError( - f"File already exists at: {path}. Cannot overwrite files using command `create`." - ) + return async def view( self, diff --git a/app/tool/terminate.py b/app/tool/terminate.py index 8c2d82ca7..bac6316a7 100644 --- a/app/tool/terminate.py +++ b/app/tool/terminate.py @@ -1,8 +1,9 @@ from app.tool.base import BaseTool -_TERMINATE_DESCRIPTION = """Terminate the interaction when the request is met OR if the assistant cannot proceed further with the task. -When you have finished all the tasks, call this tool to end the work.""" +_TERMINATE_DESCRIPTION = """Finish the current task only after verifying the requested outcome. +Use status=success when the work is complete and verified. Use status=failure when blocked. +Always include a concise summary and, for failures, the exact reason/blocker.""" class Terminate(BaseTool): @@ -15,11 +16,24 @@ class Terminate(BaseTool): "type": "string", "description": "The finish status of the interaction.", "enum": ["success", "failure"], - } + }, + "summary": { + "type": "string", + "description": "Concise final answer or completion summary shown to the user.", + }, + "reason": { + "type": "string", + "description": "Why the task is ending, especially for failure or partial completion.", + }, }, "required": ["status"], } - async def execute(self, status: str) -> str: + async def execute(self, status: str, summary: str = "", reason: str = "") -> str: """Finish the current execution""" - return f"The interaction has been completed with status: {status}" + details = [f"The interaction has been completed with status: {status}."] + if summary: + details.append(f"Summary: {summary}") + if reason: + details.append(f"Reason: {reason}") + return "\n".join(details) diff --git a/app/tool/tool_collection.py b/app/tool/tool_collection.py index 297ab6cbe..4e252db9e 100644 --- a/app/tool/tool_collection.py +++ b/app/tool/tool_collection.py @@ -69,3 +69,9 @@ def add_tools(self, *tools: BaseTool): for tool in tools: self.add_tool(tool) return self + + def without(self, disabled_tool_names: set[str]): + """Return a new collection without disabled tool names.""" + return ToolCollection( + *(tool for tool in self.tools if tool.name not in disabled_tool_names) + ) diff --git a/app/tool/user_input_tool.py b/app/tool/user_input_tool.py new file mode 100644 index 000000000..0f4cee18d --- /dev/null +++ b/app/tool/user_input_tool.py @@ -0,0 +1,72 @@ +import asyncio +import os +from typing import Optional + +import redis as redis_lib + +from app.task_context import get_current_task +from app.tool.base import BaseTool, ToolResult + + +REDIS_URL = os.getenv("CELERY_BROKER_URL", "redis://redis:6379/0") + + +class WaitForUserInput(BaseTool): + """Optionally wait for a mid-task user message from the web UI.""" + + name: str = "wait_for_user_input" + description: str = ( + "Optionally waits for a user message that may arrive while this task is running. " + "Do not use this for clarification before starting work; make reasonable assumptions " + "and continue autonomously if no message arrives before the timeout." + ) + parameters: dict = { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Optional short note explaining what information would help.", + }, + "timeout_seconds": { + "type": "integer", + "description": "Maximum seconds to wait before continuing autonomously.", + }, + }, + } + + async def execute( + self, message: Optional[str] = None, timeout_seconds: Optional[int] = None + ) -> ToolResult: + task = get_current_task() + if task is None or not getattr(task, "id", None): + return ToolResult( + output="No active task inbox is available; continue autonomously." + ) + + timeout = max(1, min(timeout_seconds or 30, 300)) + inbox_key = f"task:{task.id}:inbox" + + if message: + task.emit( + "user_input_wait", {"message": message, "timeout_seconds": timeout} + ) + + def _wait_for_message(): + client = redis_lib.from_url(REDIS_URL, decode_responses=True) + try: + return client.blpop(inbox_key, timeout=timeout) + finally: + client.close() + + result = await asyncio.to_thread(_wait_for_message) + if not result: + return ToolResult( + output=( + f"No user message arrived within {timeout} seconds. " + "Continue with the most reasonable assumption." + ) + ) + + _key, user_message = result + task.emit("user_message", {"message": user_message}) + return ToolResult(output=f"User message received: {user_message}") diff --git a/app/tool/web_search.py b/app/tool/web_search.py index b9b9e31ae..c967e9556 100644 --- a/app/tool/web_search.py +++ b/app/tool/web_search.py @@ -19,6 +19,10 @@ from app.tool.search.base import SearchItem +# Maximum seconds to wait for a single engine before declaring it failed. +_ENGINE_TIMEOUT_SECONDS = 12 + + class SearchResult(BaseModel): """Represents a single search result returned by a search engine.""" @@ -57,7 +61,7 @@ class SearchResponse(ToolResult): results: List[SearchResult] = Field( default_factory=list, description="List of search results" ) - metadata: Optional[SearchMetadata] = Field( + search_metadata: Optional[SearchMetadata] = Field( default=None, description="Metadata about the search" ) @@ -89,13 +93,13 @@ def populate_output(self) -> "SearchResponse": result_text.append(f" Content: {content_preview}") # Add metadata at the bottom if available - if self.metadata: + if self.search_metadata: result_text.extend( [ f"\nMetadata:", - f"- Total results: {self.metadata.total_results}", - f"- Language: {self.metadata.language}", - f"- Country: {self.metadata.country}", + f"- Total results: {self.search_metadata.total_results}", + f"- Language: {self.search_metadata.language}", + f"- Country: {self.search_metadata.country}", ] ) @@ -119,7 +123,7 @@ async def fetch_content(url: str, timeout: int = 10) -> Optional[str]: Extracted text content or None if fetching fails """ headers = { - "WebSearch": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" } try: @@ -157,9 +161,12 @@ class WebSearch(BaseTool): """Search the web for information using various search engines.""" name: str = "web_search" + parallel_safe: bool = True # read-only network I/O, no shared state description: str = """Search the web for real-time information about any topic. This tool returns comprehensive search results with relevant information, URLs, titles, and descriptions. - If the primary search engine fails, it automatically falls back to alternative engines.""" + It tries Google → DuckDuckGo → Bing → Baidu automatically if the primary engine fails. + IMPORTANT: This tool is the correct way to search the internet. Do NOT use python_execute with + requests/httpx to fetch search results when this tool is available.""" parameters: dict = { "type": "object", "properties": { @@ -169,8 +176,8 @@ class WebSearch(BaseTool): }, "num_results": { "type": "integer", - "description": "(optional) The number of search results to return. Default is 5.", - "default": 5, + "description": "(optional) The number of search results to return. Default is 8.", + "default": 8, }, "lang": { "type": "string", @@ -201,7 +208,7 @@ class WebSearch(BaseTool): async def execute( self, query: str, - num_results: int = 5, + num_results: int = 8, lang: Optional[str] = None, country: Optional[str] = None, fetch_content: bool = False, @@ -221,14 +228,14 @@ async def execute( """ # Get settings from config retry_delay = ( - getattr(config.search_config, "retry_delay", 60) + getattr(config.search_config, "retry_delay", 5) # 5s between engine retries if config.search_config - else 60 + else 5 ) max_retries = ( - getattr(config.search_config, "max_retries", 3) + getattr(config.search_config, "max_retries", 1) # 1 retry pass max if config.search_config - else 3 + else 1 ) # Use config values for lang and country if not specified @@ -262,7 +269,7 @@ async def execute( status="success", query=query, results=results, - metadata=SearchMetadata( + search_metadata=SearchMetadata( total_results=len(results), language=lang, country=country, @@ -280,10 +287,17 @@ async def execute( f"All search engines failed after {max_retries} retries. Giving up." ) - # Return an error response + # Return an error response with explicit guidance for the LLM return SearchResponse( query=query, - error="All search engines failed to return results after multiple retries.", + error=( + f"All search engines (Google, DuckDuckGo, Bing, Baidu) failed to return " + f"results for query '{query}' after {max_retries} retries. " + "Try rephrasing the query, breaking it into smaller sub-queries, or use " + "browser_use with go_to_url to visit a specific known URL directly. " + "Do NOT fall back to python_execute with requests — it has the same network " + "restrictions and will also fail." + ), results=[], ) @@ -292,21 +306,40 @@ async def _try_all_engines( ) -> List[SearchResult]: """Try all search engines in the configured order.""" engine_order = self._get_engine_order() - failed_engines = [] + failed_engines: List[str] = [] for engine_name in engine_order: engine = self._search_engine[engine_name] logger.info(f"🔎 Attempting search with {engine_name.capitalize()}...") - search_items = await self._perform_search_with_engine( - engine, query, num_results, search_params - ) + try: + search_items = await asyncio.wait_for( + self._perform_search_with_engine( + engine, query, num_results, search_params + ), + timeout=_ENGINE_TIMEOUT_SECONDS, + ) + except asyncio.TimeoutError: + logger.warning( + f"{engine_name.capitalize()} timed out after {_ENGINE_TIMEOUT_SECONDS}s — skipping." + ) + failed_engines.append(f"{engine_name}(timeout)") + continue + except Exception as exc: + logger.warning(f"{engine_name.capitalize()} raised {exc!r} — skipping.") + failed_engines.append(f"{engine_name}(error)") + continue if not search_items: + logger.warning( + f"{engine_name.capitalize()} returned 0 results — skipping." + ) + failed_engines.append(f"{engine_name}(empty)") continue if failed_engines: logger.info( - f"Search successful with {engine_name.capitalize()} after trying: {', '.join(failed_engines)}" + f"✅ Search successful with {engine_name.capitalize()} " + f"after failing: {', '.join(failed_engines)}" ) # Transform search items into structured results @@ -314,16 +347,14 @@ async def _try_all_engines( SearchResult( position=i + 1, url=item.url, - title=item.title - or f"Result {i+1}", # Ensure we always have a title + title=item.title or f"Result {i + 1}", description=item.description or "", source=engine_name, ) for i, item in enumerate(search_items) ] - if failed_engines: - logger.error(f"All search engines failed: {', '.join(failed_engines)}") + logger.error(f"All search engines failed: {', '.join(failed_engines)}") return [] async def _fetch_content_for_results( @@ -364,14 +395,16 @@ def _get_engine_order(self) -> List[str]: if config.search_config else "google" ) + has_explicit_fallbacks = config.search_config and hasattr( + config.search_config, "fallback_engines" + ) fallbacks = ( [engine.lower() for engine in config.search_config.fallback_engines] - if config.search_config - and hasattr(config.search_config, "fallback_engines") + if has_explicit_fallbacks else [] ) - # Start with preferred engine, then fallbacks, then remaining engines + # Start with preferred engine, then fallbacks engine_order = [preferred] if preferred in self._search_engine else [] engine_order.extend( [ @@ -380,12 +413,16 @@ def _get_engine_order(self) -> List[str]: if fb in self._search_engine and fb not in engine_order ] ) - engine_order.extend([e for e in self._search_engine if e not in engine_order]) + # Only add remaining engines when no explicit fallback list was configured + if not has_explicit_fallbacks: + engine_order.extend( + [e for e in self._search_engine if e not in engine_order] + ) return engine_order @retry( - stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10) + stop=stop_after_attempt(1), wait=wait_exponential(multiplier=1, min=1, max=10) ) async def _perform_search_with_engine( self, diff --git a/config/config.example.toml b/config/config.example.toml index 2e99bf828..d94b3cc03 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -6,6 +6,24 @@ api_key = "YOUR_API_KEY" # Your API key max_tokens = 8192 # Maximum number of tokens in the response temperature = 0.0 # Controls randomness +# enable_thinking controls reasoning/thinking mode: +# (not set) = auto-detect via LM-Studio /api/v0/models (recommended for local models) +# true = always enable thinking (useful if auto-detect misses the model) +# false = always disable thinking (useful to save tokens on fast tasks) +# enable_thinking = true + +# [llm] # LM-Studio (local) +# api_type = "openai" +# model = "qwen/qwq-32b" # Exact model id shown in LM-Studio +# base_url = "http://localhost:1234/v1" # LM-Studio default port +# api_key = "lm-studio" # Any non-empty string works +# max_tokens = 8192 +# temperature = 0.6 +# enable_thinking is auto-detected: LM-Studio's /api/v0/models reports +# "reasoning: true" for QwQ, DeepSeek-R1, Phi-4 Reasoning, Gemma 3 Thinking, etc. +# Uncomment below to override: +# enable_thinking = true + # [llm] # Amazon Bedrock # api_type = "aws" # Required # model = "us.anthropic.claude-3-7-sonnet-20250219-v1:0" # Bedrock supported modelID @@ -23,22 +41,6 @@ temperature = 0.0 # Controls randomness # temperature = 0.0 # api_version="AZURE API VERSION" #"2024-08-01-preview" -# [llm] #OLLAMA: -# api_type = 'ollama' -# model = "llama3.2" -# base_url = "http://localhost:11434/v1" -# api_key = "ollama" -# max_tokens = 4096 -# temperature = 0.0 - -# [llm] #Jiekou.AI: -# api_type = 'jiekou' -# model = "claude-sonnet-4-5-20250929" # The LLM model to use -# base_url = "https://api.jiekou.ai/openai" # API endpoint URL -# api_key = "your Jiekou.AI api key" # Your API key -# max_tokens = 64000 # Maximum number of tokens in the response -# temperature = 0.0 # Controls randomness - # Optional configuration for specific LLM models [llm.vision] model = "claude-3-7-sonnet-20250219" # The vision model to use @@ -94,14 +96,15 @@ temperature = 0.0 # Controls randomness for vision mode ## Sandbox configuration -#[sandbox] -#use_sandbox = false -#image = "python:3.12-slim" -#work_dir = "/workspace" -#memory_limit = "1g" # 512m -#cpu_limit = 2.0 -#timeout = 300 -#network_enabled = true +[sandbox] +use_sandbox = true +image = "openmanus-worker:latest" +work_dir = "/workspace" +memory_limit = "2g" +cpu_limit = 2.0 +timeout = 300 +network_enabled = true +docker_socket_enabled = true # MCP (Model Context Protocol) configuration [mcp] @@ -111,3 +114,44 @@ server_reference = "app.mcp.server" # default server module reference # Your can add additional agents into run-flow workflow to solve different-type tasks. [runflow] use_data_analysis_agent = false # The Data Analysi Agent to solve various data analysis tasks + +# Agent configuration +[agent] +# Maximum number of steps the agent can take +max_steps = 160 +# Maximum number of tool calls allowed in a single step +max_tools_per_step = 6 + +# Optional AgentMemory integration (https://github.com/rohitg00/agentmemory) +[agentmemory] +enabled = false +base_url = "http://localhost:3111" +project = "openmanus" +top_k = 5 +timeout_seconds = 8 +auto_remember_completion = true +vector_backend = "faiss" # "none", "faiss" +embedding_provider = "openai_compatible" +embedding_model = "text-embedding-nomic-embed-text-v1.5" +embedding_base_url = "" +embedding_api_key = "" +vector_index_path = "/app/workspace/agentmemory.faiss" +vector_meta_path = "/app/workspace/agentmemory_vectors.json" +hybrid_search = true +vector_weight = 0.65 +keyword_weight = 0.35 + +# Optional RL policy integration +[rl] +enabled = false +policy_mode = "base" # "base" or "rl" +policy_path = "research/openmanus-rl/artifacts/policy/latest/policy.md" +metadata_path = "research/openmanus-rl/artifacts/policy/latest/metadata.json" + +# Optional DeepSpec research integration +[deepspec] +enabled = false +repo_url = "https://github.com/deepseek-ai/DeepSpec" +checkout_dir = "research/deepspec" +mode = "research" +target_model = "Qwen/Qwen3-4B" diff --git a/context/engine.py b/context/engine.py new file mode 100644 index 000000000..893205197 --- /dev/null +++ b/context/engine.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import asyncio +import json +import queue +from typing import Any, Dict, List, Optional + +from core.task import Task + + +class ContextEngine: + """Builds prompt context from task state and recent events (no vector store).""" + + @classmethod + def build( + cls, + task: Task, + agent_role: Optional[str] = None, + step_type: Optional[str] = None, + budget: int = 4000, + ) -> Dict[str, Any]: + """Assemble context sections and enforce a simple character budget.""" + events = cls._snapshot_events(task) + + hard_facts = cls._collect_hard_facts(events) + recent_events = cls._collect_recent(events) + process_summary = cls._summarize(events, hard_facts) + + context = { + "agent_role": agent_role, + "step_type": step_type, + "hard_facts": hard_facts, + "recent_events": recent_events, + "process_summary": process_summary, + } + + return cls._enforce_budget(context, budget) + + @staticmethod + def _snapshot_events(task: Task) -> List[Dict[str, Any]]: + """Copy events without mutating the queue (supports asyncio.Queue and queue.Queue).""" + q = task.event_queue + items: List[Any] = [] + if isinstance(q, queue.Queue): + try: + items = list(q.queue) # type: ignore[attr-defined] + except Exception: + items = [] + elif isinstance(q, asyncio.Queue): + try: + items = list(q._queue) # type: ignore[attr-defined] + except Exception: + items = [] + return [e for e in items if isinstance(e, dict) and "type" in e] + + @staticmethod + def _collect_hard_facts(events: List[Dict[str, Any]]) -> Dict[str, Any]: + """Extract stable context: user goal and plan.""" + plan = None + plan_steps: List[Any] = [] + user_goal = None + + for ev in events: + etype = ev.get("type") + data = ev.get("data", {}) + if etype == "plan.done": + plan = data.get("plan") or plan + elif etype == "plan.step": + plan_steps.append(data.get("step")) + elif etype == "thought" and not user_goal: + # Heuristic: first thought often echoes the goal + user_goal = data.get("content") or data + + if plan is None and plan_steps: + plan = plan_steps + + return {"user_goal": user_goal, "plan": plan} + + @staticmethod + def _collect_recent(events: List[Dict[str, Any]], limit: int = 5) -> List[Any]: + """Collect recent tool-related events.""" + tool_events = [ + ev + for ev in events + if ev.get("type") + in {"tool.call", "tool.result", "step_result", "execute.step.start"} + ] + return tool_events[-limit:] + + @staticmethod + def _summarize(events: List[Dict[str, Any]], hard_facts: Dict[str, Any]) -> str: + tool_calls = sum(1 for e in events if e.get("type") == "tool.call") + tool_results = sum(1 for e in events if e.get("type") == "tool.result") + steps = sum( + 1 for e in events if e.get("type") in {"plan.step", "execute.step.start"} + ) + plan_known = bool(hard_facts.get("plan")) + return ( + f"Steps seen: {steps}; tool calls: {tool_calls}; tool results: {tool_results}; " + f"plan_available: {plan_known}" + ) + + @staticmethod + def _enforce_budget(context: Dict[str, Any], budget: int) -> Dict[str, Any]: + """Trim context to fit a simple character budget.""" + text = json.dumps(context, ensure_ascii=False) + if len(text) <= budget: + return context + + # Trim recent events first, then hard facts strings + recent = context.get("recent_events") or [] + while recent and len(json.dumps(context, ensure_ascii=False)) > budget: + recent.pop(0) + context["recent_events"] = recent + + def _truncate_str(value: Any, max_len: int) -> Any: + if isinstance(value, str) and len(value) > max_len: + return value[-max_len:] + return value + + context["process_summary"] = _truncate_str(context.get("process_summary"), 300) + hard = context.get("hard_facts") or {} + if isinstance(hard, dict): + hard["user_goal"] = _truncate_str(hard.get("user_goal"), 500) + context["hard_facts"] = hard + return context + + +__all__ = ["ContextEngine"] diff --git a/core/task.py b/core/task.py new file mode 100644 index 000000000..86b7ec75a --- /dev/null +++ b/core/task.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import asyncio +import queue +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Optional, Union + + +class TaskStatus(str, Enum): + CREATED = "CREATED" + RUNNING = "RUNNING" + DONE = "DONE" + FAILED = "FAILED" + INTERRUPTED = "INTERRUPTED" + + +EventQueue = Union[asyncio.Queue, queue.Queue] + + +@dataclass +class Task: + """Generic task container for agents/tools.""" + + id: str + status: TaskStatus = TaskStatus.CREATED + interrupt_flag: bool = False + event_queue: EventQueue = field(default_factory=asyncio.Queue) + _loop: Optional[asyncio.AbstractEventLoop] = field( + default=None, repr=False, compare=False + ) + + def __post_init__(self) -> None: + # Capture the running loop if we are created inside one; helps thread-safe emits. + if isinstance(self.event_queue, asyncio.Queue) and self._loop is None: + try: + self._loop = asyncio.get_running_loop() + except RuntimeError: + self._loop = None + + def emit(self, type: str, data: Any) -> None: + """Push an event into the queue.""" + event = {"type": type, "data": data} + + if isinstance(self.event_queue, asyncio.Queue): + loop = self._loop + if loop is None or not loop.is_running(): + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + + if loop and loop.is_running(): + # Schedule thread-safe put when an event loop is available. + loop.call_soon_threadsafe(self.event_queue.put_nowait, event) + else: + # Fallback for cases without a running loop. + self.event_queue.put_nowait(event) + elif isinstance(self.event_queue, queue.Queue): + self.event_queue.put_nowait(event) + else: + raise TypeError( + "event_queue must be an asyncio.Queue or queue.Queue instance" + ) + + def interrupt(self) -> None: + """Mark task as interrupted.""" + self.interrupt_flag = True + if self.status not in (TaskStatus.DONE, TaskStatus.FAILED): + self.status = TaskStatus.INTERRUPTED + + def is_interrupted(self) -> bool: + """Return whether the task has been interrupted.""" + return self.interrupt_flag + + +__all__ = ["Task", "TaskStatus", "EventQueue"] diff --git a/core/task_registry.py b/core/task_registry.py new file mode 100644 index 000000000..d1c1f6914 --- /dev/null +++ b/core/task_registry.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import asyncio +import os +import threading +import uuid +from typing import Optional + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from core.task import Task, TaskStatus +from server.models import Base, TaskORM + + +class TaskRegistry: + """Database-backed registry for Task objects using SQLAlchemy.""" + + def __init__(self, db_url: Optional[str] = None) -> None: + self._lock = threading.RLock() + self.db_url = db_url or os.getenv( + "DATABASE_URL", + "postgresql+psycopg2://postgres:postgres@localhost:5432/openmanus", + ) + self.engine = create_engine(self.db_url) + Base.metadata.create_all(self.engine) + self.SessionLocal = sessionmaker(bind=self.engine, expire_on_commit=False) + + def _to_task(self, orm: TaskORM) -> Task: + status = ( + TaskStatus(orm.status) + if orm.status in TaskStatus._value2member_map_ + else TaskStatus.CREATED + ) + # Use fresh queue per retrieval; events are in-memory only + return Task( + id=str(orm.task_id), + status=status, + event_queue=asyncio.Queue(), + interrupt_flag=False, + ) + + def create_task( + self, task_id: Optional[str] = None, input: Optional[dict] = None, **task_kwargs + ) -> Task: + with self._lock: + tid = task_id or str(uuid.uuid4()) + session = self.SessionLocal() + try: + orm = TaskORM(task_id=tid, status=TaskStatus.CREATED.value, input=input) + session.add(orm) + session.commit() + task = Task(id=tid, status=TaskStatus.CREATED, **task_kwargs) + return task + finally: + session.close() + + def get_task(self, task_id: str) -> Optional[Task]: + with self._lock: + session = self.SessionLocal() + try: + orm = session.get(TaskORM, task_id) + if orm is None: + return None + return self._to_task(orm) + finally: + session.close() + + def update_task(self, task: Task, result: Optional[dict] = None) -> Task: + with self._lock: + session = self.SessionLocal() + try: + orm = session.get(TaskORM, task.id) + if orm is None: + orm = TaskORM(task_id=task.id) + session.add(orm) + orm.status = ( + task.status.value + if isinstance(task.status, TaskStatus) + else str(task.status) + ) + if hasattr(task, "input"): + orm.input = getattr(task, "input") + orm.result = result if result is not None else orm.result + session.commit() + return task + finally: + session.close() + + def interrupt_task(self, task_id: str) -> Optional[Task]: + with self._lock: + session = self.SessionLocal() + try: + orm = session.get(TaskORM, task_id) + if orm is None: + return None + orm.status = TaskStatus.INTERRUPTED.value + session.commit() + task = self._to_task(orm) + task.interrupt() + return task + finally: + session.close() + + +__all__ = ["TaskRegistry"] diff --git a/core/task_runner.py b/core/task_runner.py new file mode 100644 index 000000000..7bbb492ea --- /dev/null +++ b/core/task_runner.py @@ -0,0 +1,34 @@ +from typing import Any, Awaitable, Optional + +from app.agent.base import TaskInterrupted +from core.task import Task, TaskStatus + + +async def run_with_status( + task: Task, work: Awaitable[Any], mark_running: bool = True +) -> Any: + """Execute an awaitable and update task status with unified rules.""" + + def _set_status(status: TaskStatus, reason: Optional[str] = None) -> None: + task.status = status + payload = {"status": status.value} + if reason: + payload["reason"] = reason + task.emit("task.status", payload) + + if mark_running: + _set_status(TaskStatus.RUNNING) + + try: + result = await work + _set_status(TaskStatus.DONE) + return result + except TaskInterrupted: + _set_status(TaskStatus.INTERRUPTED) + raise + except Exception as exc: + _set_status(TaskStatus.FAILED, reason=str(exc)) + raise + + +__all__ = ["run_with_status"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..5d60ce136 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,89 @@ +services: + frontend: + build: + context: ./frontend + image: openmanus-frontend:latest + ports: + - "3000:5173" + depends_on: + - web + volumes: + - ./frontend:/app + - /app/node_modules + + web: + build: . + image: openmanus-web:latest + command: uvicorn server.api:app --host 0.0.0.0 --port 8000 + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql+psycopg2://postgres:postgres@postgres:5432/openmanus + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/0 + - OPENMANUS_HOST_WORKSPACE_ROOT=/home/mohamed/OpenManus/workspace + - OPENMANUS_SINGLE_CONVERSATION=false + - OPENMANUS_DEFAULT_CONVERSATION_ID=main + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./config:/app/config + - ./app:/app/app + - ./server:/app/server + - ./workspace:/app/workspace + - /var/run/docker.sock:/var/run/docker.sock + + worker: + build: . + image: openmanus-worker:latest + command: celery -A server.celery_app.celery_app worker -l info + environment: + - DATABASE_URL=postgresql+psycopg2://postgres:postgres@postgres:5432/openmanus + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/0 + - OPENMANUS_HOST_WORKSPACE_ROOT=/home/mohamed/OpenManus/workspace + - OPENMANUS_SINGLE_CONVERSATION=false + - OPENMANUS_DEFAULT_CONVERSATION_ID=main + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./config:/app/config + - ./app:/app/app + - ./server:/app/server + - ./workspace:/app/workspace + - /var/run/docker.sock:/var/run/docker.sock + + redis: + image: redis:latest + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + postgres: + image: postgres:15 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=openmanus + volumes: + - pgdata:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + pgdata: diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000..289e710c0 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +.vite +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 000000000..f524b2258 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "printWidth": 150, + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "auto", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..e2a8dfc7b --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-slim + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm install + +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host"] diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 000000000..b9444a489 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/libs/utils", + "ui": "@/components/ui", + "lib": "@/libs", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 000000000..d94e7deb7 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 000000000..123a90778 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + OpenManus + + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 000000000..42f35ab37 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,7902 @@ +{ + "name": "openmanus-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openmanus-frontend", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "^4.1.3", + "@prisma/client": "^6.5.0", + "@radix-ui/react-alert-dialog": "^1.1.6", + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.5", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slider": "^1.2.3", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", + "@tailwindcss/vite": "^4.1.11", + "@types/node": "^24.0.4", + "bcryptjs": "^3.0.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "github-markdown-css": "^5.8.1", + "jose": "^6.0.10", + "json-schema-to-ts": "^3.1.1", + "lodash": "^4.17.21", + "lucide-react": "^0.483.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-hook-form": "^7.55.0", + "react-markdown": "^10.1.0", + "react-router-dom": "^6.30.1", + "react-syntax-highlighter": "^15.6.1", + "react-use-websocket": "^4.13.0", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.2", + "tailwind-merge": "^3.3.1", + "zod": "^3.24.2", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.29.0", + "@tailwindcss/postcss": "^4.1.10", + "@tailwindcss/typography": "^0.5.16", + "@types/lodash": "^4.17.18", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@types/react-syntax-highlighter": "^15.5.13", + "@vitejs/plugin-react": "^4.5.2", + "autoprefixer": "^10.4.21", + "eslint": "^9.29.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.2.0", + "prettier": "^3.6.1", + "prettier-plugin-tailwindcss": "^0.6.13", + "tailwindcss": "^4.1.10", + "tw-animate-css": "^1.3.4", + "typescript": "~5.8.3", + "typescript-eslint": "^8.34.1", + "vite": "^7.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", + "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz", + "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz", + "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz", + "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.3.tgz", + "integrity": "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz", + "integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@prisma/client": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.10.1.tgz", + "integrity": "sha512-Re4pMlcUsQsUTAYMK7EJ4Bw2kg3WfZAAlr8GjORJaK4VOP6LxRQUQ1TuLnxcF42XqGkWQ36q5CQF1yVadANQ6w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz", + "integrity": "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.14", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", + "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", + "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", + "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.13.tgz", + "integrity": "sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz", + "integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", + "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", + "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", + "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz", + "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", + "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", + "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz", + "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz", + "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz", + "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz", + "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", + "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", + "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz", + "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz", + "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz", + "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz", + "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz", + "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", + "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", + "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz", + "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", + "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.10.tgz", + "integrity": "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.10" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.10.tgz", + "integrity": "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.10", + "@tailwindcss/oxide-darwin-arm64": "4.1.10", + "@tailwindcss/oxide-darwin-x64": "4.1.10", + "@tailwindcss/oxide-freebsd-x64": "4.1.10", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.10", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.10", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.10", + "@tailwindcss/oxide-linux-x64-musl": "4.1.10", + "@tailwindcss/oxide-wasm32-wasi": "4.1.10", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.10", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.10" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.10.tgz", + "integrity": "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.10.tgz", + "integrity": "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.10.tgz", + "integrity": "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.10.tgz", + "integrity": "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.10.tgz", + "integrity": "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.10.tgz", + "integrity": "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.10.tgz", + "integrity": "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.10.tgz", + "integrity": "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.10.tgz", + "integrity": "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.10.tgz", + "integrity": "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.10", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz", + "integrity": "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.10.tgz", + "integrity": "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.10.tgz", + "integrity": "sha512-B+7r7ABZbkXJwpvt2VMnS6ujcDoR2OOcFaqrLIo1xbcdxje4Vf+VgJdBzNNbrAjBj/rLZ66/tlQ1knIGNLKOBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.10", + "@tailwindcss/oxide": "4.1.10", + "postcss": "^8.4.41", + "tailwindcss": "4.1.10" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", + "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "tailwindcss": "4.1.11" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite/node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/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/lodash": { + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.18.tgz", + "integrity": "sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", + "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", + "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/type-utils": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.35.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", + "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", + "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", + "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", + "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.35.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", + "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.19", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "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/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.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "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": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/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/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.173", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.173.tgz", + "integrity": "sha512-2bFhXP2zqSfQHugjqJIDFVwa+qIxyNApenmXTp9EjaKtdPrES5Qcn9/aSFy/NaP2E+fWG/zxKu/LBvY36p5VNQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", + "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.1", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.29.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/github-markdown-css": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.8.1.tgz", + "integrity": "sha512-8G+PFvqigBQSWLQjyzgpa2ThD9bo7+kDsriUIidGcRhXgmcaAWUIpCZf8DavJgc+xifjbCG+GvMyWr0XMXmc7g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "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/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/hastscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-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-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jose": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", + "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "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-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.483.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.483.0.tgz", + "integrity": "sha512-WldsY17Qb/T3VZdMnVQ9C3DDIP7h1ViDTHVdVGnLZcvHNg30zH/MTQ04RTORjexoGmpsXroiQXZ4QyR0kBy0FA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-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-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/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "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/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "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/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz", + "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.13.tgz", + "integrity": "sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.58.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.58.1.tgz", + "integrity": "sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/react-use-websocket": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.13.0.tgz", + "integrity": "sha512-anMuVoV//g2N76Wxqvqjjo1X48r9Np3y1/gMl7arX84tAPXdy5R7sB5lO5hvCzQRYjqXwV8XMAiEBOUbyrZFrw==", + "license": "MIT" + }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", + "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.44.0", + "@rollup/rollup-android-arm64": "4.44.0", + "@rollup/rollup-darwin-arm64": "4.44.0", + "@rollup/rollup-darwin-x64": "4.44.0", + "@rollup/rollup-freebsd-arm64": "4.44.0", + "@rollup/rollup-freebsd-x64": "4.44.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", + "@rollup/rollup-linux-arm-musleabihf": "4.44.0", + "@rollup/rollup-linux-arm64-gnu": "4.44.0", + "@rollup/rollup-linux-arm64-musl": "4.44.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-musl": "4.44.0", + "@rollup/rollup-linux-s390x-gnu": "4.44.0", + "@rollup/rollup-linux-x64-gnu": "4.44.0", + "@rollup/rollup-linux-x64-musl": "4.44.0", + "@rollup/rollup-win32-arm64-msvc": "4.44.0", + "@rollup/rollup-win32-ia32-msvc": "4.44.0", + "@rollup/rollup-win32-x64-msvc": "4.44.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "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/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/sonner": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.5.tgz", + "integrity": "sha512-YwbHQO6cSso3HBXlbCkgrgzDNIhws14r4MO87Ofy+cV2X7ES4pOoAK3+veSmVTvqNx1BWUxlhPmZzP00Crk2aQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", + "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.9" + } + }, + "node_modules/style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "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/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", + "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "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==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.4.tgz", + "integrity": "sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", + "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.35.0", + "@typescript-eslint/parser": "8.35.0", + "@typescript-eslint/utils": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.0.tgz", + "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.2", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which": { + "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/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz", + "integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 000000000..50bdb6145 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,76 @@ +{ + "name": "openmanus-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^4.1.3", + "@prisma/client": "^6.5.0", + "@radix-ui/react-alert-dialog": "^1.1.6", + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.5", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slider": "^1.2.3", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", + "@tailwindcss/vite": "^4.1.11", + "@types/node": "^24.0.4", + "bcryptjs": "^3.0.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "github-markdown-css": "^5.8.1", + "jose": "^6.0.10", + "json-schema-to-ts": "^3.1.1", + "lodash": "^4.17.21", + "lucide-react": "^0.483.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-hook-form": "^7.55.0", + "react-markdown": "^10.1.0", + "react-router-dom": "^6.30.1", + "react-syntax-highlighter": "^15.6.1", + "react-use-websocket": "^4.13.0", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "sonner": "^2.0.2", + "tailwind-merge": "^3.3.1", + "zod": "^3.24.2", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.29.0", + "@tailwindcss/postcss": "^4.1.10", + "@tailwindcss/typography": "^0.5.16", + "@types/lodash": "^4.17.18", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@types/react-syntax-highlighter": "^15.5.13", + "@vitejs/plugin-react": "^4.5.2", + "autoprefixer": "^10.4.21", + "eslint": "^9.29.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.2.0", + "prettier": "^3.6.1", + "prettier-plugin-tailwindcss": "^0.6.13", + "tailwindcss": "^4.1.10", + "tw-animate-css": "^1.3.4", + "typescript": "~5.8.3", + "typescript-eslint": "^8.34.1", + "vite": "^7.0.0" + } +} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 000000000..6a83185b6 --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +}; + +export default config; diff --git a/frontend/public/logo.jpg b/frontend/public/logo.jpg new file mode 100644 index 000000000..634b8f685 Binary files /dev/null and b/frontend/public/logo.jpg differ diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx new file mode 100644 index 000000000..5ed21a27d --- /dev/null +++ b/frontend/src/app.tsx @@ -0,0 +1,994 @@ +import { ConfirmDialog } from '@/components/block/confirm'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuItem, +} from '@/components/ui/sidebar'; +import { useConversations } from '@/hooks/use-conversations'; +import { cn } from '@/libs/utils'; +import AdminPage from '@/pages/AdminPage'; +import AuthPage from '@/pages/AuthPage'; +import ConversationSettingsPage from '@/pages/ConversationSettingsPage'; +import HomePage from '@/pages/HomePage'; +import TaskDetailPage from '@/pages/TaskDetailPage'; +import { getMe, logout, type User } from '@/services/auth'; +import { deleteConversation, updateConversationSettings } from '@/services/conversations'; +import { ejectModel, listModels, loadModel, queryModels, type ModelOption } from '@/services/models'; +import { + HomeIcon, + LoaderCircle, + LoaderIcon, + LogOut, + MessageSquare, + Plus, + PowerOff, + Settings, + Shield, + SlidersHorizontal, + Trash2, +} from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { Link, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; +import { SidebarProvider } from './components/ui/sidebar'; + +type ConnectionStyle = 'lm-studio' | 'ollama' | 'openai' | 'custom'; + +type ConnectionProfile = { + id: string; + name: string; + host: string; + apiKey: string; + style: ConnectionStyle; + chatPath: string; + modelsPath: string; + loadPath: string; + unloadPath: string; + defaultModel: string; + defaultContextWindow: string; +}; + +const DEFAULT_PROFILE: ConnectionProfile = { + id: 'default', + name: 'Default', + host: 'http://127.0.0.1:1234', + apiKey: '', + style: 'lm-studio', + chatPath: '', + modelsPath: '', + loadPath: '', + unloadPath: '', + defaultModel: '', + defaultContextWindow: '', +}; + +const STORAGE = { + selectedModel: 'openmanus.selectedModel', + connectionProfiles: 'openmanus.connection.profiles', + activeProfileId: 'openmanus.connection.activeProfileId', +}; + +function styleDefaultHost(style: ConnectionStyle): string { + if (style === 'lm-studio') return 'http://127.0.0.1:1234'; + if (style === 'ollama') return 'http://127.0.0.1:11434'; + if (style === 'openai') return 'https://api.openai.com/v1'; + return ''; +} + +function matchesStyle(style: ConnectionStyle, apiType: string): boolean { + const normalized = String(apiType || '').toLowerCase(); + if (style === 'lm-studio') return normalized === 'lmstudio' || normalized === 'lm-studio'; + if (style === 'ollama') return normalized === 'ollama'; + if (style === 'openai') return normalized === 'openai'; + // For custom connections, do not hide provider-specific models. + return true; +} + +function loadProfilesFromStorage(): ConnectionProfile[] { + try { + const raw = localStorage.getItem(STORAGE.connectionProfiles); + if (!raw) return [DEFAULT_PROFILE]; + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed) || !parsed.length) return [DEFAULT_PROFILE]; + return parsed; + } catch { + return [DEFAULT_PROFILE]; + } +} + +function App() { + const [user, setUser] = useState(undefined); + const [models, setModels] = useState([]); + const [selectedModel, setSelectedModel] = useState( + localStorage.getItem(STORAGE.selectedModel) || '', + ); + const [isEjectingModel, setIsEjectingModel] = useState(false); + const [isLoadingModel, setIsLoadingModel] = useState(false); + const [requestedContextWindow, setRequestedContextWindow] = useState(''); + + const [profiles, setProfiles] = useState(() => + typeof window !== 'undefined' ? loadProfilesFromStorage() : [DEFAULT_PROFILE], + ); + const [activeProfileId, setActiveProfileId] = useState( + localStorage.getItem(STORAGE.activeProfileId) || 'default', + ); + const [pendingProfileId, setPendingProfileId] = useState( + localStorage.getItem(STORAGE.activeProfileId) || 'default', + ); + const [newProfileName, setNewProfileName] = useState(''); + const [showProfileOptions, setShowProfileOptions] = useState(false); + const [profileOptionsMode, setProfileOptionsMode] = useState<'create' | 'edit'>('edit'); + + const [isConnectionSettingsOpen, setIsConnectionSettingsOpen] = useState(false); + const [connectionHostDraft, setConnectionHostDraft] = useState(DEFAULT_PROFILE.host); + const [connectionApiKeyDraft, setConnectionApiKeyDraft] = useState(DEFAULT_PROFILE.apiKey); + const [connectionStyleDraft, setConnectionStyleDraft] = useState('lm-studio'); + const [connectionChatPathDraft, setConnectionChatPathDraft] = useState(''); + const [connectionModelsPathDraft, setConnectionModelsPathDraft] = useState(''); + const [connectionLoadPathDraft, setConnectionLoadPathDraft] = useState(''); + const [connectionUnloadPathDraft, setConnectionUnloadPathDraft] = useState(''); + const [defaultModelDraft, setDefaultModelDraft] = useState(''); + const [defaultContextWindowDraft, setDefaultContextWindowDraft] = useState(''); + const [isVerifyingConnection, setIsVerifyingConnection] = useState(false); + + const { + conversations, + activeConversationId, + createNewConversation, + refreshConversations, + removeConversation, + setActiveConversationId, + } = useConversations(); + const location = useLocation(); + const navigate = useNavigate(); + + const activeProfile = + profiles.find(profile => profile.id === activeProfileId) || profiles[0] || DEFAULT_PROFILE; + const currentTaskId = location.pathname.startsWith('/tasks/') + ? location.pathname.split('/').pop() + : undefined; + + const filteredModels = useMemo( + () => models.filter(model => matchesStyle(connectionStyleDraft, model.api_type)), + [models, connectionStyleDraft], + ); + + const groupedFilteredModels = useMemo(() => { + const groups = new Map(); + for (const model of filteredModels) { + const base = String(model.base_model || model.id || 'model'); + const bucket = groups.get(base) || []; + bucket.push(model); + groups.set(base, bucket); + } + return [...groups.entries()]; + }, [filteredModels]); + + const selectedModelInfo = useMemo( + () => filteredModels.find(model => model.id === selectedModel), + [filteredModels, selectedModel], + ); + const selectedModelState = String(selectedModelInfo?.state || '').toLowerCase(); + const selectedModelLoaded = + selectedModelState === 'loaded' || selectedModelState === 'running'; + + const styleEndpoints = useMemo(() => { + if (connectionStyleDraft === 'lm-studio') { + return { + chat: '/v1/chat/completions', + models: '/v1/models', + load: '/api/v1/models/load', + unload: '/api/v1/models/unload', + }; + } + if (connectionStyleDraft === 'ollama') { + return { + chat: '/v1/chat/completions', + models: '/v1/models', + load: '/api/tags', + unload: '/api/ps', + }; + } + if (connectionStyleDraft === 'openai') { + return { + chat: '/v1/chat/completions', + models: '/v1/models', + load: '(n/a)', + unload: '(n/a)', + }; + } + return { + chat: connectionChatPathDraft || '/v1/chat/completions', + models: connectionModelsPathDraft || '/v1/models', + load: connectionLoadPathDraft || '/api/v1/models/load', + unload: connectionUnloadPathDraft || '/api/v1/models/unload', + }; + }, [ + connectionStyleDraft, + connectionChatPathDraft, + connectionModelsPathDraft, + connectionLoadPathDraft, + connectionUnloadPathDraft, + ]); + + const fetchModelsForProfile = async (profile: ConnectionProfile): Promise => { + try { + const items = await queryModels({ + host: profile.host || styleDefaultHost(profile.style), + api_key: profile.apiKey || '', + style: profile.style, + models_path: profile.modelsPath || '', + }); + if (items.length) return items; + } catch { + // fallback below + } + return listModels(); + }; + + useEffect(() => { + localStorage.setItem(STORAGE.connectionProfiles, JSON.stringify(profiles)); + }, [profiles]); + + useEffect(() => { + localStorage.setItem(STORAGE.activeProfileId, activeProfileId); + }, [activeProfileId]); + + useEffect(() => { + setPendingProfileId(activeProfileId); + }, [activeProfileId]); + + useEffect(() => { + getMe().then(res => setUser(res.user || null)); + }, []); + + useEffect(() => { + if (!user) return; + refreshConversations(); + fetchModelsForProfile(activeProfile).then(items => { + setModels(items); + const initial = selectedModel || activeProfile.defaultModel; + if (initial && items.some(model => model.id === initial)) { + setSelectedModel(initial); + localStorage.setItem(STORAGE.selectedModel, initial); + } else if (items[0]?.id) { + setSelectedModel(items[0].id); + localStorage.setItem(STORAGE.selectedModel, items[0].id); + } + }); + }, [user, refreshConversations, activeProfile, activeProfile.defaultModel, selectedModel]); + + useEffect(() => { + setConnectionHostDraft(activeProfile.host || styleDefaultHost(activeProfile.style)); + setConnectionApiKeyDraft(activeProfile.apiKey || ''); + setConnectionStyleDraft(activeProfile.style || 'lm-studio'); + setConnectionChatPathDraft(activeProfile.chatPath || ''); + setConnectionModelsPathDraft(activeProfile.modelsPath || ''); + setConnectionLoadPathDraft(activeProfile.loadPath || ''); + setConnectionUnloadPathDraft(activeProfile.unloadPath || ''); + setDefaultModelDraft(activeProfile.defaultModel || ''); + setDefaultContextWindowDraft(activeProfile.defaultContextWindow || ''); + }, [activeProfileId, activeProfile]); + + useEffect(() => { + if (!filteredModels.length) return; + if (!selectedModel || !filteredModels.some(model => model.id === selectedModel)) { + setSelectedModel(filteredModels[0].id); + localStorage.setItem(STORAGE.selectedModel, filteredModels[0].id); + } + }, [filteredModels, selectedModel]); + + useEffect(() => { + const activeConversation = conversations.find(item => item.id === activeConversationId); + const value = activeConversation?.settings?.requested_context_window; + setRequestedContextWindow(value ? String(value) : activeProfile.defaultContextWindow || ''); + }, [conversations, activeConversationId, activeProfile.defaultContextWindow]); + + if (user === undefined) { + return ( +
+ Loading... +
+ ); + } + + if (!user) { + return setUser(signedInUser)} />; + } + + const handleNewConversation = async () => { + const conversation = await createNewConversation(); + navigate(`/conversations/${conversation.id}`); + }; + + const handleLogout = async () => { + await logout(); + setUser(null); + setActiveConversationId(undefined); + }; + + const handleDeleteConversation = async (conversationId: string) => { + await deleteConversation(conversationId); + removeConversation(conversationId); + if (location.pathname.includes(conversationId)) navigate('/'); + }; + + const handleSaveContextWindow = async () => { + if (!activeConversationId) return; + const trimmed = requestedContextWindow.trim(); + const parsed = trimmed ? Number.parseInt(trimmed, 10) : null; + if (trimmed && (!parsed || parsed <= 0)) { + toast.error('Context window must be a positive integer'); + return; + } + try { + await updateConversationSettings(activeConversationId, { + requested_context_window: parsed, + }); + await refreshConversations(); + toast.success('Context window updated'); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Could not update context window'); + } + }; + + const handleEjectModel = async () => { + if (isEjectingModel) return; + setIsEjectingModel(true); + try { + const res = await ejectModel(selectedModel || undefined); + if (!res.ok) { + toast.error(res.detail || 'Could not eject model'); + return; + } + toast.success(`Ejected model${res.instance_id ? `: ${res.instance_id}` : ''}`); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Could not eject model'); + } finally { + setIsEjectingModel(false); + const refreshed = await fetchModelsForProfile(activeProfile).catch(() => []); + if (refreshed.length) setModels(refreshed); + } + }; + + const handleLoadModel = async () => { + if (isLoadingModel || !selectedModel) return; + setIsLoadingModel(true); + try { + const contextLength = Number.parseInt(requestedContextWindow || '', 10); + const res = await loadModel({ + host: activeProfile.host || styleDefaultHost(activeProfile.style), + api_key: activeProfile.apiKey || '', + style: activeProfile.style, + model: selectedModel, + context_length: Number.isFinite(contextLength) && contextLength > 0 ? contextLength : undefined, + }); + if (!res.ok) { + toast.error(res.detail || 'Could not load model'); + return; + } + toast.success(`Loaded model: ${selectedModel}`); + const refreshed = await fetchModelsForProfile(activeProfile).catch(() => []); + if (refreshed.length) setModels(refreshed); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Could not load model'); + } finally { + setIsLoadingModel(false); + } + }; + + const saveDraftToActiveProfile = () => { + setProfiles(prev => + prev.map(profile => + profile.id === activeProfileId + ? { + ...profile, + host: connectionHostDraft, + apiKey: connectionApiKeyDraft, + style: connectionStyleDraft, + chatPath: connectionChatPathDraft, + modelsPath: connectionModelsPathDraft, + loadPath: connectionLoadPathDraft, + unloadPath: connectionUnloadPathDraft, + defaultModel: defaultModelDraft, + defaultContextWindow: defaultContextWindowDraft, + } + : profile, + ), + ); + if (defaultModelDraft) { + setSelectedModel(defaultModelDraft); + localStorage.setItem(STORAGE.selectedModel, defaultModelDraft); + } + setIsConnectionSettingsOpen(false); + toast.success('Profile saved'); + }; + + const createProfileFromDraft = () => { + const name = newProfileName.trim(); + if (!name) { + toast.error('Profile name is required'); + return; + } + const id = `${Date.now()}`; + const profile: ConnectionProfile = { + id, + name, + host: connectionHostDraft, + apiKey: connectionApiKeyDraft, + style: connectionStyleDraft, + chatPath: connectionChatPathDraft, + modelsPath: connectionModelsPathDraft, + loadPath: connectionLoadPathDraft, + unloadPath: connectionUnloadPathDraft, + defaultModel: defaultModelDraft, + defaultContextWindow: defaultContextWindowDraft, + }; + setProfiles(prev => [profile, ...prev]); + setActiveProfileId(id); + setNewProfileName(''); + toast.success('Profile created'); + }; + + const deleteActiveProfile = () => { + if (activeProfileId === 'default') { + toast.error('Default profile cannot be deleted'); + return; + } + const next = profiles.filter(profile => profile.id !== activeProfileId); + setProfiles(next.length ? next : [DEFAULT_PROFILE]); + setActiveProfileId(next[0]?.id || 'default'); + toast.success('Profile deleted'); + }; + + const applySelectedProfile = async () => { + if (!pendingProfileId || pendingProfileId === activeProfileId) return; + const target = profiles.find(profile => profile.id === pendingProfileId); + if (!target) return; + try { + const items = await fetchModelsForProfile(target); + setModels(items); + setActiveProfileId(pendingProfileId); + toast.success('Profile selected and models refreshed'); + } catch { + toast.error('Profile selected, but model refresh failed'); + } + }; + + const verifyConnection = async () => { + if (isVerifyingConnection) return; + setIsVerifyingConnection(true); + try { + const response = await fetch('/api/connection/verify', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + host: connectionHostDraft, + api_key: connectionApiKeyDraft, + style: connectionStyleDraft, + models_path: connectionModelsPathDraft, + }), + }); + const data = await response.json().catch(() => ({})); + if (!response.ok || !data?.ok) { + toast.error(data?.detail || 'Connection verify failed'); + return; + } + const queried = await queryModels({ + host: connectionHostDraft, + api_key: connectionApiKeyDraft, + style: connectionStyleDraft, + models_path: connectionModelsPathDraft, + }); + if (queried.length) { + setModels(queried); + } + toast.success( + `Connection OK (${queried.length || data.models_count || 0} models) via ${data.url || 'target'}`, + ); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Connection verify failed'); + } finally { + setIsVerifyingConnection(false); + } + }; + + return ( + +
+ + +
+ OpenManus + + + GitHub + + + +
+ + +
+ + + Model + +
+ Style: {connectionStyleDraft} +
+
+
+ Active profile: {activeProfile.name} +
+ +
+
+ + + +
+
+ Model status:{' '} + + {selectedModelLoaded ? 'Loaded' : 'Unloaded'} + +
+
+
Requested context window
+
+ setRequestedContextWindow(event.target.value)} + /> + +
+
+ Received:{' '} + {(() => { + const activeConversation = conversations.find(item => item.id === activeConversationId); + const value = activeConversation?.context?.received_window; + const source = activeConversation?.context?.received_window_source; + const sourceLabel = + source === 'lmstudio_load' + ? 'confirmed by LM Studio' + : source + ? 'inferred fallback' + : 'unknown source'; + return value + ? `${value.toLocaleString()} (${sourceLabel})` + : `-- (${sourceLabel})`; + })()} +
+
+
+
+ + + Conversations + + + {conversations.map(item => ( + +
+ setActiveConversationId(item.id)} + className={cn( + 'flex min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-2 text-sm', + (activeConversationId === item.id || + currentTaskId === item.latest_task_id) && + 'bg-muted', + )} + > + + {item.title} + = 0.9 + ? 'text-amber-500' + : 'text-muted-foreground', + )} + > + {typeof item.context?.usage_ratio === 'number' + ? `${Math.round(item.context.usage_ratio * 100)}%` + : '--'} + + + + + + +
+
+ ))} +
+
+
+
+ + +
+
+
+
{user.name}
+ {user.role === 'admin' && ( + + Admin + + )} +
+
{user.email}
+
+ {user.role === 'admin' && ( + + )} + +
+
+
+ + + + + Connection Settings + + Create and load connection profiles with host, style, and endpoint behavior. + + + +
+
+ +
+ + + +
+
+ +
+ +
+ + +
+
+ + {showProfileOptions && ( + <> + {profileOptionsMode === 'create' && ( +
+ +
+ setNewProfileName(event.target.value)} + placeholder="e.g. OpenAI Prod" + className="h-9 text-sm" + /> + +
+
+ )} + +
+
Connection
+
+ +
+ setConnectionHostDraft(event.target.value)} + placeholder="http://127.0.0.1:1234" + className="h-9 text-sm" + /> + +
+
+ +
+
+ + setConnectionApiKeyDraft(event.target.value)} + placeholder="sk-... (optional)" + className="h-9 text-sm" + /> +
+
+ +
+ + +
+ +
+
Endpoints for this style
+
chat: {styleEndpoints.chat}
+
models: {styleEndpoints.models}
+
load: {styleEndpoints.load}
+
unload: {styleEndpoints.unload}
+
+ + {connectionStyleDraft === 'custom' && ( +
+
Custom endpoint paths
+ setConnectionChatPathDraft(event.target.value)} + placeholder="/v1/chat/completions" + className="h-8 text-xs placeholder:text-muted-foreground/70" + /> + setConnectionModelsPathDraft(event.target.value)} + placeholder="/v1/models" + className="h-8 text-xs placeholder:text-muted-foreground/70" + /> + setConnectionLoadPathDraft(event.target.value)} + placeholder="/api/v1/models/load" + className="h-8 text-xs placeholder:text-muted-foreground/70" + /> + setConnectionUnloadPathDraft(event.target.value)} + placeholder="/api/v1/models/unload" + className="h-8 text-xs placeholder:text-muted-foreground/70" + /> +
+ )} + +
+ + +
+ +
+ + setDefaultContextWindowDraft(event.target.value)} + placeholder="e.g. 128000" + className="h-9 text-sm" + /> +
+
+ + )} +
+ + + + + +
+
+ +
+ + } /> + } + /> + } /> + } /> + } /> + +
+ +
+
+ ); +} + +export default App; diff --git a/frontend/src/components/block/confirm/index.tsx b/frontend/src/components/block/confirm/index.tsx new file mode 100644 index 000000000..42007d43c --- /dev/null +++ b/frontend/src/components/block/confirm/index.tsx @@ -0,0 +1,137 @@ +import React, { useState } from 'react'; +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import type { FieldValues, UseFormReturn } from 'react-hook-form'; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { cn } from '@/libs/utils'; +import { Button } from '@/components/ui/button'; + +interface ConfirmDialogStore { + open: boolean; + content: React.ReactNode; + className?: string; + buttonText?: { cancel?: string; confirm?: string; loading?: string }; + operations?: (props: { setOpen: (value: boolean) => void }) => React.ReactNode[]; + onConfirm?: () => void | Promise; + setOpen: (value: boolean) => void; + setContent: (content: React.ReactNode) => void; + setOnConfirm: (onConfirm?: () => void) => void; + setButtonText: (buttonText?: { cancel?: string; confirm?: string; loading?: string }) => void; + setOperations: (operations?: (props: { setOpen: (value: boolean) => void }) => React.ReactNode[]) => void; + setClassName: (className?: string) => void; +} + +const useConfirmDialogStore = create()( + devtools(set => ({ + open: false, + content: null, + className: undefined, + buttonText: { cancel: 'Cancel', confirm: 'Confirm', loading: 'Processing' }, + operations: () => [], + onConfirm: undefined, + setOpen: (value: boolean) => set({ open: value }), + setContent: content => set({ content }), + setOnConfirm: onConfirm => set({ onConfirm }), + setButtonText: buttonText => set({ buttonText }), + setOperations: operations => set({ operations }), + setClassName: className => set({ className }), + })), +); + +const ConfirmDialog = () => { + const { open, content, buttonText, operations, onConfirm, setOpen, className } = useConfirmDialogStore(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleConfirm = async () => { + setIsSubmitting(true); + try { + await onConfirm?.(); + } finally { + setIsSubmitting(false); + setOpen(false); + } + }; + return ( + !isSubmitting && setOpen(value)}> + + + + +
{content}
+ + {operations ? ( + operations({ setOpen }) + ) : ( + <> + + {buttonText?.confirm && ( + + )} + + )} + +
+
+ ); +}; + +ConfirmDialog.displayName = 'ConfirmDialog'; + +function confirm(props: { + content: React.ReactNode | ((props: { form?: UseFormReturn }) => React.ReactNode); + className?: string; + buttonText?: { cancel?: string; confirm?: string; loading?: string }; + operations?: (props: { setOpen: (value: boolean) => void }) => React.ReactNode[]; + onConfirm?: (formData?: F) => R | Promise; + form?: UseFormReturn; +}) { + const { setOpen, setContent, setOnConfirm, setButtonText, setOperations, setClassName } = useConfirmDialogStore.getState(); + + const wrappedOnConfirm = async () => { + if (props.form) { + const formData = props.form.getValues(); + await props.onConfirm?.(formData); + } else { + await props.onConfirm?.(); + } + }; + + setContent(typeof props.content === 'function' ? props.content({ form: props.form }) : props.content); + setOnConfirm(wrappedOnConfirm); + setOperations(props.operations); + setButtonText(props.buttonText); + setClassName(props.className); + setOpen(true); +} + +export async function asyncConfirm(props: { + content: React.ReactNode | ((props: { form?: UseFormReturn }) => React.ReactNode); + buttonText?: { cancel?: string; confirm?: string }; + operations?: (props: { setOpen: (value: boolean) => void }) => React.ReactNode[]; + onConfirm?: (formData?: F) => R | Promise; + form?: UseFormReturn; +}) { + const promise = new Promise(resolve => { + confirm({ + content: props.content, + buttonText: props.buttonText, + operations: props.operations, + form: props.form, + onConfirm: async (formData?: F) => { + if (!props.onConfirm) { + resolve(undefined as any); + return; + } + const res = await props.onConfirm(formData as any); + resolve(res); + }, + }); + }); + return promise; +} + +export { ConfirmDialog, confirm }; diff --git a/frontend/src/components/block/markdown/index.tsx b/frontend/src/components/block/markdown/index.tsx new file mode 100644 index 000000000..f0a63969f --- /dev/null +++ b/frontend/src/components/block/markdown/index.tsx @@ -0,0 +1,27 @@ +import { cn } from '@/libs/utils'; +import React from 'react'; +import ReactMarkdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; + +export const Markdown: React.FC<{ children: string | null; className?: string }> = ({ children, className }) => { + return ( +
+ { + return ( + + {children} + + ); + }, + }} + > + {children} + +
+ ); +}; diff --git a/frontend/src/components/features/chat/input/index.tsx b/frontend/src/components/features/chat/input/index.tsx new file mode 100644 index 000000000..f8b7541f9 --- /dev/null +++ b/frontend/src/components/features/chat/input/index.tsx @@ -0,0 +1,109 @@ +import { confirm } from '@/components/block/confirm'; +import { Button } from '@/components/ui/button'; +import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Textarea } from '@/components/ui/textarea'; +import { PauseCircle, Rocket, Send } from 'lucide-react'; +import { useState } from 'react'; + +interface ChatInputProps { + taskId?: string; + status?: 'idle' | 'thinking' | 'terminating' | 'completed'; + onSubmit?: (value: { taskId?: string; prompt: string }) => Promise; + onTerminate?: () => Promise; +} + +export const ChatInput = ({ taskId, status = 'idle', onSubmit, onTerminate }: ChatInputProps) => { + const [value, setValue] = useState(''); + + const handleKeyDown = async (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + if (status === 'terminating' || !value.trim()) { + return; + } + await onSubmit?.({ taskId, prompt: value.trim() }); + setValue(''); + } + }; + + const handleSendClick = async () => { + const v = value.trim(); + if ((status === 'thinking' && !v) || status === 'terminating') { + confirm({ + content: ( + + Terminate Task + Are you sure you want to terminate this task? + + ), + onConfirm: async () => { + await onTerminate?.(); + }, + buttonText: { + cancel: 'Cancel', + confirm: 'Terminate', + loading: 'Terminating...', + }, + }); + return; + } + if (v) { + await onSubmit?.({ prompt: v }); + setValue(''); + } + }; + + return ( +
+
+ {status !== 'idle' && ( +
+ +
+ )} +
+